Compare commits

...

63 Commits

Author SHA1 Message Date
github-actions[bot]
aa2881f3c2 Update version to v1.4.121 and commit 2024-12-13 21:17:35 +00:00
Eugen Eisler
82379ee6ec Merge pull request #1200 from mattjoyce/bugfix/1157-inputvars
Fix: Mask input token to prevent var substitution in patterns
2024-12-14 02:46:38 +05:30
Matt Joyce
e795055d13 Fix: Mask input token to prevent var substitution in patterns 2024-12-14 06:57:53 +11:00
Daniel Miessler
5b6d7e27b6 Added new instruction trick. 2024-12-11 13:54:33 -08:00
github-actions[bot]
c6dc13ef7f Update version to v1.4.120 and commit 2024-12-10 12:23:12 +00:00
Eugen Eisler
7e6a760623 Merge pull request #1189 from mattjoyce/bugfix/1157-inputvars
Add --input-has-vars flag to control variable substitution in input
2024-12-10 17:52:16 +05:30
Matt Joyce
01519d7486 Add --input-has-vars flag to control variable substitution in input
- Add InputHasVars field to ChatRequest struct
- Only process template variables in user input when flag is set
- Fixes issue with Ansible/Jekyll templates that use {{var}} syntax

This change makes template variable substitution in user input opt-in
via the --input-has-vars flag, preserving literal curly braces by
default.
2024-12-10 18:49:18 +11:00
Eugen Eisler
f5f50cc4c9 Merge pull request #1182 from jessefmoore/main
analyze_risk pattern
2024-12-07 23:57:01 +01:00
Jesse Moore
9226e95d18 analyze_risk pattern
Created a pattern to analyze 3rd party vendor risk.
2024-12-07 11:48:00 -08:00
github-actions[bot]
2d8b46b878 Update version to v1.4.119 and commit 2024-12-07 11:24:51 +00:00
Eugen Eisler
fbd6083079 Merge pull request #1181 from mattjoyce/bugfix/1169-symlinks
Bugfix/1169 symlinks
2024-12-07 12:23:53 +01:00
Matt Joyce
0320e17652 Revert "Update version to v..1 and commit"
This reverts commit ec5ed689bb.
2024-12-07 19:14:20 +11:00
Matt Joyce
09fb913279 Fix #1169: Add robust handling for paths and symlinks in GetAbsolutePath 2024-12-07 19:04:39 +11:00
github-actions[bot]
ec5ed689bb Update version to v..1 and commit 2024-12-07 03:58:53 +00:00
github-actions[bot]
43ca0dccf7 Update version to v1.4.118 and commit 2024-12-05 09:34:13 +00:00
Eugen Eisler
fcfcf55610 Merge pull request #1174 from mattjoyce/curly-brace-templates
Curly brace templates
2024-12-05 10:33:16 +01:00
Eugen Eisler
188235efc5 Merge pull request #1179 from sluosapher/main
added a new pattern create_newsletter_entry
2024-12-05 10:32:23 +01:00
Song Luo
fdd1d614b2 added a new pattern create_newsletter_entry 2024-12-03 20:05:25 -05:00
Matt Joyce
fc67dea243 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-03 13:33:45 +11:00
Matt Joyce
efd363d5fb Revert "Fix pattern file usage without stdin"
This reverts commit 744ec0824b.
2024-12-03 13:33:45 +11:00
Matt Joyce
a7d6de1661 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-03 13:33:40 +11:00
github-actions[bot]
c0ade48648 Update version to v1.4.117 and commit 2024-11-30 19:45:48 +00:00
Eugen Eisler
7fd4fa4742 fix: close #1173 2024-11-30 20:44:31 +01:00
github-actions[bot]
41b2e66c5c Update version to v1.4.116 and commit 2024-11-28 18:34:53 +00:00
Eugen Eisler
ed657383fb chore: cleanup style 2024-11-28 19:34:07 +01:00
github-actions[bot]
4d5d8d8b30 Update version to v1.4.115 and commit 2024-11-28 17:39:30 +00:00
Eugen Eisler
e9a75528ab chore: cleanup style 2024-11-28 18:36:01 +01:00
Eugen Eisler
c5ec4b548a fix: use the custom message and then piped one 2024-11-27 14:10:19 +01:00
Eugen Eisler
8e87529638 fix: use the custom message and then piped one 2024-11-27 14:10:18 +01:00
xssdoctor
ca33208fa1 Merge pull request #1168 from johnconnor-sec/main 2024-11-26 19:19:45 -05:00
John Connor
3f8bca8728 Update README.md 2024-11-26 19:15:18 -05:00
John Connor
ba56c33cf6 Update README.md 2024-11-26 19:12:29 -05:00
Jonathan Dunn
6ee4fdd366 updated readme 2024-11-26 17:53:58 -05:00
github-actions[bot]
30af189ae3 Update version to v1.4.114 and commit 2024-11-26 22:14:16 +00:00
Eugen Eisler
be998ff588 Merge pull request #1164 from MegaGrindStone/fix-nil-pointer-message
fix: provide default message content to avoid nil pointer dereference
2024-11-26 23:13:20 +01:00
github-actions[bot]
6bb3238e6d Update version to v1.4.113 and commit 2024-11-26 21:45:39 +00:00
Eugen Eisler
dfcd29593d Merge pull request #1166 from danielmiessler/dependabot/npm_and_yarn/web/npm_and_yarn-8ea39cdf54
build(deps-dev): bump @sveltejs/kit from 2.6.1 to 2.8.4 in /web in the npm_and_yarn group across 1 directory
2024-11-26 22:44:55 +01:00
dependabot[bot]
63b357168e build(deps-dev): bump @sveltejs/kit
Bumps the npm_and_yarn group with 1 update in the /web directory: [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit).


Updates `@sveltejs/kit` from 2.6.1 to 2.8.4
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.8.4/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-26 21:40:49 +00:00
github-actions[bot]
317a4309f7 Update version to v1.4.112 and commit 2024-11-26 21:40:17 +00:00
Eugen Eisler
eceb10b725 Merge pull request #1165 from johnconnor-sec/main
feat: Fabric Web UI
2024-11-26 22:39:16 +01:00
MegaGrindStone
34f508fd82 fix: provide default message content to avoid nil pointer dereference 2024-11-27 02:32:07 +07:00
John Connor
9fa8634083 Update README.md 2024-11-26 09:13:04 -05:00
John Connor
a3ea63c1f9 Update Obsidian.md 2024-11-26 09:07:57 -05:00
John Connor
097b3eb0ba Update Obsidian.md 2024-11-26 09:07:30 -05:00
John
30f37ea633 john 2024-11-26 08:53:48 2024-11-26 08:54:12 -05:00
github-actions[bot]
23b495c8f7 Update version to v..1 and commit 2024-11-26 13:52:03 +00:00
John
e7f2d48437 john 2024-11-26 08:44:20 2024-11-26 08:50:31 -05:00
John
7043f78f1f john 2024-11-26 08:40:21 2024-11-26 08:50:31 -05:00
github-actions[bot]
f2cc718f49 Update version to v..1 and commit 2024-11-26 11:41:16 +00:00
github-actions[bot]
edb814c9f0 Update version to v1.4.111 and commit 2024-11-26 09:43:27 +00:00
Eugen Eisler
21de69b7d9 ci: Integrate code formating 2024-11-26 10:42:26 +01:00
github-actions[bot]
d4b5c3b8d5 Update version to v1.4.110 and commit 2024-11-26 08:52:27 +00:00
Eugen Eisler
afb5857699 Merge pull request #1135 from mrtnrdl/main
Add `extract_recipe`
2024-11-26 09:51:41 +01:00
github-actions[bot]
153b8217fd Update version to v..1 and commit 2024-11-25 19:04:07 +00:00
Martin Riedel
beeba6989a Merge branch 'main' into main 2024-11-25 20:02:52 +01:00
github-actions[bot]
666a1d32a3 Update version to v1.4.109 and commit 2024-11-24 15:31:52 +00:00
Eugen Eisler
4ed512b8d4 Merge pull request #1157 from mattjoyce/curly-brace-templates
fix: process template variables in raw input
2024-11-24 16:30:55 +01:00
Matt Joyce
af16494be1 fax: raw mode was doubling user input, because it's now already embeded in pattern
streamlined some context staging
2024-11-23 10:45:38 +11:00
Matt Joyce
9afa397c27 fix : template.go will handle missing var in stdin imput too.
echo 'Hello {{name}}' | ./fabric -v=noname:World
missing required variable: name
2024-11-23 08:34:58 +11:00
Matt Joyce
58f9d3c89c fix: process template variables in raw input
Process template variables ({{var}}) consistently in both pattern files
and raw input messages. Previously variables were only processed when
using pattern files.

- Add template variable processing for raw input in BuildSession
- Initialize messageContent explicitly
- Remove errantly committed build artifact (fabric binary in previous commit)
2024-11-23 08:27:56 +11:00
Daniel Miessler
7732b6fe55 Added analyze_mistakes. 2024-11-22 10:01:40 -08:00
martin riedel
b758a27b93 add extract_recipe to easily extract the necessary information from cooking-videos 2024-11-13 21:06:32 +01:00
github-actions[bot]
81d765a34c Update version to v..1 and commit 2024-11-13 07:57:50 +00:00
175 changed files with 17889 additions and 1374 deletions

View File

@@ -63,6 +63,10 @@ jobs:
- name: Update version.nix file
run: |
echo "\"${{ env.new_version }}\"" > pkgs/fabric/version.nix
- name: Format source codes
run: |
go fmt ./...
- name: Update gomod2nix.toml file
run: |
@@ -73,6 +77,7 @@ jobs:
git add version.go
git add pkgs/fabric/version.nix
git add gomod2nix.toml
git add .
if ! git diff --staged --quiet; then
git commit -m "Update version to ${{ env.new_tag }} and commit $commit_hash"
else

168
.gitignore vendored
View File

@@ -22,7 +22,7 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
@@ -166,3 +166,169 @@ 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/
web/node_modules
# Output
web/.output
web/.vercel
web/.svelte-kit
web/build
# OS
web/.DS_Store
web/Thumbs.db
# Env
web/.env
web/.env.*
web/!.env.example
web/!.env.test
# Vite
web/vite.config.js.timestamp-*
web/vite.config.ts.timestamp-*
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
web/logs
web/*.log
web/npm-debug.log*
web/yarn-debug.log*
web/yarn-error.log*
web/lerna-debug.log*
web/.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
web/report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
web/pids
web/*.pid
web/*.seed
web/*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
web/lib-cov
# Coverage directory used by tools like istanbul
web/coverage
web/*.lcov
# nyc test coverage
web/.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
web/.grunt
# Bower dependency directory (https://bower.io/)
web/bower_components
# node-waf configuration
web/.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
web/node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web/web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
web/.npm
# Optional eslint cache
web/.eslintcache
# Optional stylelint cache
web/.stylelintcache
# Microbundle cache
web/.rpt2_cache/
web/.rts2_cache_cjs/
web/.rts2_cache_es/
web/.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
web/.env
web/.env.development.local
web/.env.test.local
web/.env.production.local
web/.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
web/.next
web/out
# Nuxt.js build / generate output
web/.nuxt
web/dist
# Gatsby files
web/.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
web/.vuepress/dist
# vuepress v2.x temp and cache directory
web/.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
web/.vscode-test
# yarn v2
web/.yarn/cache
web/.yarn/unplugged
web/.yarn/build-state.yml
web/.yarn/install-state.gz
web/.pnp.*
### Node Patch ###
# Serverless Webpack directories
web/.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
web/.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node

View File

@@ -26,6 +26,7 @@
[Meta](#meta)
![Screenshot of fabric](images/fabric-summarize.png)
</div>
## Navigation
@@ -56,6 +57,7 @@
- [`to_pdf`](#to_pdf)
- [`to_pdf` Installation](#to_pdf-installation)
- [pbpaste](#pbpaste)
- [Web Interface](#Web_Interface)
- [Meta](#meta)
- [Primary contributors](#primary-contributors)
@@ -64,9 +66,9 @@
## Updates
> [!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."`
> 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."`
## What and why
@@ -82,10 +84,10 @@ Fabric was created to address this by enabling everyone to granularly apply AI t
Keep in mind that many of these were recorded when Fabric was Python-based, so remember to use the current [install instructions](#Installation) below.
* [Network Chuck](https://www.youtube.com/watch?v=UbDyjIIGaxQ)
* [David Bombal](https://www.youtube.com/watch?v=vF-MQmVxnCs)
* [My Own Intro to the Tool](https://www.youtube.com/watch?v=wPEyyigh10g)
* [More Fabric YouTube Videos](https://www.youtube.com/results?search_query=fabric+ai)
- [Network Chuck](https://www.youtube.com/watch?v=UbDyjIIGaxQ)
- [David Bombal](https://www.youtube.com/watch?v=vF-MQmVxnCs)
- [My Own Intro to the Tool](https://www.youtube.com/watch?v=wPEyyigh10g)
- [More Fabric YouTube Videos](https://www.youtube.com/results?search_query=fabric+ai)
## Philosophy
@@ -155,6 +157,7 @@ go install github.com/danielmiessler/fabric@latest
You may need to set some environment variables in your `~/.bashrc` on linux or `~/.zshrc` file on mac to be able to run the `fabric` command. Here is an example of what you can add:
For Intel based macs or linux
```bash
# Golang environment variables
export GOROOT=/usr/local/go
@@ -165,6 +168,7 @@ export PATH=$GOPATH/bin:$GOROOT/bin:$HOME/.local/bin:$PATH
```
for Apple Silicon based macs
```bash
# Golang environment variables
export GOROOT=$(brew --prefix go)/libexec
@@ -173,14 +177,18 @@ export PATH=$GOPATH/bin:$GOROOT/bin:$HOME/.local/bin:$PATH
```
### Setup
Now run the following command
```bash
# Run the setup to set up your directories and keys
fabric --setup
```
If everything works you are good to go.
### Add aliases for all patterns
In order to add aliases for all your patterns and use them directly as commands ie. `summarize` instead of `fabric --pattern summarize`
You can add the following to your `.zshrc` or `.bashrc` file.
@@ -189,10 +197,10 @@ You can add the following to your `.zshrc` or `.bashrc` file.
for pattern_file in $HOME/.config/fabric/patterns/*; do
# Get the base name of the file (i.e., remove the directory path)
pattern_name=$(basename "$pattern_file")
# Create an alias in the form: alias pattern_name="fabric --pattern pattern_name"
alias_command="alias $pattern_name='fabric --pattern $pattern_name'"
# Evaluate the alias command to add it to the current shell
eval "$alias_command"
done
@@ -202,9 +210,11 @@ yt() {
fabric -y "$video_link" --transcript
}
```
This also creates a `yt` alias that allows you to use `yt https://www.youtube.com/watch?v=4b0iet22VIk` to get your transcripts.
#### Save your files in markdown using aliases
If in addition to the above aliases you would like to have the option to save the output to your favourite markdown note vault like Obsidian then instead of the above add the following to your `.zshrc` or `.bashrc` file:
```bash
@@ -244,7 +254,7 @@ yt() {
}
```
This will allow you to use the patterns as aliases like in the above for example `summarize` instead of `fabric --pattern summarize --stream`, however if you pass in an extra argument like this `summarize "my_article_title"` your output will be saved in the destination that you set in `obsidian_base="/path/to/obsidian"` in the following format `YYYY-MM-DD-my_article_title.md` where the date gets autogenerated for you.
This will allow you to use the patterns as aliases like in the above for example `summarize` instead of `fabric --pattern summarize --stream`, however if you pass in an extra argument like this `summarize "my_article_title"` your output will be saved in the destination that you set in `obsidian_base="/path/to/obsidian"` in the following format `YYYY-MM-DD-my_article_title.md` where the date gets autogenerated for you.
You can tweak the date format by tweaking the `date_stamp` format.
### Migration
@@ -268,11 +278,13 @@ Then [set your environmental variables](#environmental-variables) as shown above
### Upgrading
The great thing about Go is that it's super easy to upgrade. Just run the same command you used to install it in the first place and you'll always get the latest version.
```bash
go install github.com/danielmiessler/fabric@latest
```
## Usage
Once you have it all set up, here's how to use it.
```bash
@@ -320,6 +332,7 @@ Application Options:
--printcontext= Print context
--printsession= Print session
--readability Convert HTML input into a clean, readable view
--serve Initiate the API server
--dry-run Show what would be sent to the model without actually sending it
--version Print current version
@@ -401,7 +414,6 @@ When you're ready to use them, copy them into:
You can then use them like any other Patterns, but they won't be public unless you explicitly submit them as Pull Requests to the Fabric project. So don't worry—they're private to you.
This feature works with all openai and ollama models but does NOT work with claude. You can specify your model with the -m flag
## Helper Apps
@@ -459,6 +471,29 @@ You can also create an alias by editing `~/.bashrc` or `~/.zshrc` and adding the
alias pbpaste='xclip -selection clipboard -o'
```
## Web Interface
Fabric now includes a built-in web interface that provides a GUI alternative to the command-line interface and an out-of-the-box website for those who want to get started with web development or blogging.
You can use this app as a GUI interface for Fabric, a ready to go blog-site, or a website template for your own projects.
The `web/src/lib/content` directory includes starter `.obsidian/` and `templates/` directories, allowing you to open up the `web/src/lib/content/` directory as an [Obsidian.md](https://obsidian.md) vault. You can place your posts in the posts directory when you're ready to publish.
### Installing
The GUI can be installed by navigating to the `web` directory and using `npm install`, `pnpm install`, or your favorite package manager. Then simply run the development server to start the app. 
_You will need to run fabric in a separate terminal with the `fabric --serve` command._
**From the fabric project `web/` directory:**
```shell
npm run dev
## or ##
pnpm run dev
## or your equivalent
```
## Meta
> [!NOTE]
@@ -467,6 +502,7 @@ alias pbpaste='xclip -selection clipboard -o'
- _Jonathan Dunn_ for being the absolute MVP dev on the project, including spearheading the new Go version, as well as the GUI! All this while also being a full-time medical doctor!
- _Caleb Sima_ for pushing me over the edge of whether to make this a public project or not.
- _Eugen Eisler_ and _Frederick Ros_ for their invaluable contributions to the Go version
- _David Peters_ for his work on the web interface.
- _Joel Parish_ for super useful input on the project's Github directory structure..
- _Joseph Thacker_ for the idea of a `-c` context flag that adds pre-created context in the `./config/fabric/` directory to all Pattern queries.
- _Jason Haddix_ for the idea of a stitch (chained Pattern) to filter content using a local model before sending on to a cloud model, i.e., cleaning customer data using `llama2` before sending on to `gpt-4` for analysis.

View File

@@ -51,6 +51,7 @@ func Cli(version string) (err error) {
}
if currentFlags.Serve {
registry.ConfigureVendors()
err = restapi.Serve(registry, currentFlags.ServeAddress)
return
}

View File

@@ -55,6 +55,7 @@ type Flags struct {
PrintContext string `long:"printcontext" description:"Print context"`
PrintSession string `long:"printsession" description:"Print session"`
HtmlReadability bool `long:"readability" description:"Convert HTML input into a clean, readable view"`
InputHasVars bool `long:"input-has-vars" description:"Apply variables to user input"`
DryRun bool `long:"dry-run" description:"Show what would be sent to the model without actually sending it"`
Serve bool `long:"serve" description:"Serve the Fabric Rest API"`
ServeAddress string `long:"address" description:"The address to bind the REST API" default:":8080"`
@@ -63,8 +64,6 @@ type Flags struct {
// Init Initialize flags. returns a Flags struct and an error
func Init() (ret *Flags, err error) {
var message string
ret = &Flags{}
parser := flags.NewParser(ret, flags.Default)
var args []string
@@ -75,21 +74,19 @@ func Init() (ret *Flags, err error) {
info, _ := os.Stdin.Stat()
pipedToStdin := (info.Mode() & os.ModeCharDevice) == 0
//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 {
//fmt.Printf("piped: %v\n", args)
if message, err = readStdin(); err != nil {
var pipedMessage string
if pipedMessage, err = readStdin(); err != nil {
return
}
} else if len(args) > 0 {
//fmt.Printf("no piped: %v\n", args)
message = args[len(args)-1]
} else {
//fmt.Printf("no data: %v\n", args)
message = ""
ret.Message = AppendMessage(ret.Message, pipedMessage)
}
ret.Message = message
return
}
@@ -130,6 +127,7 @@ func (o *Flags) BuildChatRequest(Meta string) (ret *common.ChatRequest, err erro
SessionName: o.Session,
PatternName: o.Pattern,
PatternVariables: o.PatternVariables,
InputHasVars: o.InputHasVars,
Meta: Meta,
}

View File

@@ -12,6 +12,7 @@ type ChatRequest struct {
Message *goopenai.ChatCompletionMessage
Language string
Meta string
InputHasVars bool
}
type ChatOptions struct {

73
common/utils.go Normal file
View File

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

View File

@@ -10,6 +10,7 @@ import (
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins/ai"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/danielmiessler/fabric/plugins/template"
)
const NoSessionPatternUserMessages = "no session, pattern or user messages provided"
@@ -30,6 +31,15 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (s
return
}
vendorMessages := session.GetVendorMessages()
if len(vendorMessages) == 0 {
if session.Name != "" {
err = o.db.Sessions.SaveSession(session)
}
err = fmt.Errorf("no messages provided")
return
}
if opts.Model == "" {
opts.Model = o.model
}
@@ -72,8 +82,8 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (s
return
}
func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *fsdb.Session, err error) {
// If a session name is provided, retrieve it from the database
if request.SessionName != "" {
var sess *fsdb.Session
if sess, err = o.db.Sessions.Get(request.SessionName); err != nil {
@@ -89,6 +99,7 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
session.Append(&goopenai.ChatCompletionMessage{Role: common.ChatMessageRoleMeta, Content: request.Meta})
}
// if a context name is provided, retrieve it from the database
var contextContent string
if request.ContextName != "" {
var ctx *fsdb.Context
@@ -99,24 +110,35 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
contextContent = ctx.Content
}
// Process any template variables in the message content (user input)
// Double curly braces {{variable}} indicate template substitution
// Ensure we have a message before processing, other wise we'll get an error when we pass to pattern.go
if request.Message == nil {
request.Message = &goopenai.ChatCompletionMessage{
Role: goopenai.ChatMessageRoleUser,
Content: " ",
}
}
//if there is no input from stdin
var messageContent string
if request.Message != nil {
messageContent = request.Message.Content
// Now we know request.Message is not nil, process template variables
if request.InputHasVars {
request.Message.Content, err = template.ApplyTemplate(request.Message.Content, request.PatternVariables, "")
if err != nil {
return nil, err
}
}
var patternContent string
if request.PatternName != "" {
pattern, err := o.db.Patterns.GetApplyVariables(request.PatternName, request.PatternVariables, messageContent)
if err != nil {
return nil, fmt.Errorf("could not get pattern %s: %v", request.PatternName, err)
}
patternContent = pattern.Pattern
pattern, err := o.db.Patterns.GetApplyVariables(request.PatternName, request.PatternVariables, request.Message.Content)
// pattrn will now contain user input, and all variables will be resolved, or errored
if err != nil {
return nil, fmt.Errorf("could not get pattern %s: %v", request.PatternName, err)
}
patternContent = pattern.Pattern
}
systemMessage := strings.TrimSpace(contextContent) + strings.TrimSpace(patternContent)
if request.Language != "" {
systemMessage = fmt.Sprintf("%s. Please use the language '%s' for the output.", systemMessage, request.Language)
@@ -125,7 +147,8 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
if raw {
if request.Message != nil {
if systemMessage != "" {
request.Message.Content = systemMessage + request.Message.Content
request.Message.Content = systemMessage
// system contains pattern which contains user input
}
} else {
if systemMessage != "" {

View File

@@ -0,0 +1,33 @@
# IDENTITY and PURPOSE
You are an advanced AI with a 2,128 IQ and you are an expert in understanding and analyzing thinking patterns, mistakes that came out of them, and anticipating additional mistakes that could exist in current thinking.
# STEPS
1. Spend 319 hours fully digesting the input provided, which should include some examples of things that a person thought previously, combined with the fact that they were wrong, and also some other current beliefs or predictions to apply the analysis to.
2. Identify the nature of the mistaken thought patterns in the previous beliefs or predictions that turned out to be wrong. Map those in 32,000 dimensional space.
4. Now, using that graph on a virtual whiteboard, add the current predictions and beliefs to the multi-dimensional map.
5. Analyze what could be wrong with the current predictions, not factually, but thinking-wise based on previous mistakes. E.g. "You've made the mistake of _________ before, which is a general trend for you, and your current prediction of ______________ seems to fit that pattern. So maybe adjust your probability on that down by 25%.
# OUTPUT
- In a section called PAST MISTAKEN THOUGHT PATTERNS, create a list 15-word bullets outlining the main mental mistakes that were being made before.
- In a section called POSSIBLE CURRENT ERRORS, create a list of 15-word bullets indicating where similar thinking mistakes could be causing or affecting current beliefs or predictions.
- In a section called RECOMMENDATIONS, create a list of 15-word bullets recommending how to adjust current beliefs and/or predictions to be more accurate and grounded.
# OUTPUT INSTRUCTIONS
- Only output Markdown.
- Do not give warnings or notes; only output the requested sections.
- Do not start items with the same opening words.
- Ensure you follow ALL these instructions when creating your output.
# INPUT
INPUT:

View File

@@ -0,0 +1,81 @@
# IDENTITY and PURPOSE
You are tasked with conducting a risk assessment of a third-party vendor, which involves analyzing their compliance with security and privacy standards. Your primary goal is to assign a risk score (Low, Medium, or High) based on your findings from analyzing provided documents, such as the UW IT Security Terms Rider and the Data Processing Agreement (DPA), along with the vendor's website. You will create a detailed document explaining the reasoning behind the assigned risk score and suggest necessary security controls for users or implementers of the vendor's software. Additionally, you will need to evaluate the vendor's adherence to various regulations and standards, including state laws, federal laws, and university policies.
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
# STEPS
- Conduct a risk assessment of the third-party vendor.
- Assign a risk score of Low, Medium, or High.
- Create a document explaining the reasoning behind the risk score.
- Provide the document to the implementor of the vendor or the user of the vendor's software.
- Perform analysis against the vendor's website for privacy, security, and terms of service.
- Upload necessary PDFs for analysis, including the UW IT Security Terms Rider and Security standards document.
# OUTPUT INSTRUCTIONS
- The only output format is Markdown.
- Ensure you follow ALL these instructions when creating your output.
# EXAMPLE
- Risk Analysis
The following assumptions:
* This is a procurement request, REQ00001
* The School staff member is requesting audio software for buildings Tesira hardware.
* The vendor will not engage UW Security Terms.
* The data used is for audio layouts locally on specialized computer.
* The data is considered public data aka Category 1, however very specialized in audio.
Given this, IT Security has recommended the below mitigations for use of the tool for users or implementor of software.
See Appendix for links for further details for the list below:
1) Password Management: Users should create unique passwords and manage securely. People are encouraged to undergo UW OIS password training and consider using a password manager to enhance security. Its crucial not to reuse their NETID password for the vendor account.
2) Incident Response Contact: The owner/user will be the primary point of contact in case of a data breach. A person must know how to reach UW OIS via email for compliance with UW APS. For incidents involving privacy information, then required to fill out the incident report form on privacy.uw.edu.
3) Data Backup: Its recommended to regularly back up. Ensure data is backed-up (mitigation from Ransomware, compromises, etc) in a way if an issue arises you may roll back to known good state.
Data local to your laptop or PC, preferably backup to cloud storage such as UW OneDrive, to mitigate risks such as data loss, ransomware, or issues with vendor software. Details on storage options are available on itconnect.uw.edu and specific link in below Appendix.
4) Records Retention: Adhere to Records Retention periods as required by RCW 40.14.050. Further guidance can be found on finance.uw.edu/recmgt/retentionschedules.
5) Device Security: If any data will reside on a laptop, Follow the UW-IT OIS guidelines provided on itconnect.uw.edu for securing laptops.
6) Software Patching: Routinely patch the vendor application. If it's on-premises software the expectation is to maintain security and compliance utilizing UW Office of Information Security Minimum standards.
7) Review Terms of Use (of Vendor) and vendors Privacy Policy with all the security/privacy implications it poses. Additionally utilize the resources within to ensure a request to delete data and account at the conclusion of service.
- IN CONCLUSION
This is not a comprehensive list of Risks.
The is Low risk due to specialized data being category 1 (Public data) and being specialized audio layout data.
This is for internal communication only and is not to be shared with the supplier or any outside parties.
# INPUT

View File

@@ -0,0 +1,20 @@
# Identity and Purpose
You are a custom GPT designed to create newsletter sections in the style of Frontend Weekly.
# Step-by-Step Process:
1. The user will provide article text.
2. Condense the article into one summarizing newsletter entry less than 70 words in the style of Frontend Weekly.
3. Generate a concise title for the entry, focus on the main idea or most important fact of the article
# Tone and Style Guidelines:
* Third-Party Narration: The newsletter should sound like its being narrated by an outside observer, someone who is both knowledgeable, unbiased and calm. Focus on the facts or main opinions in the original article. Creates a sense of objectivity and adds a layer of professionalism.
* Concise: Maintain brevity and clarity. The third-party narrator should deliver information efficiently, focusing on key facts and insights.
# Output Instructions:
Your final output should be a polished, newsletter-ready paragraph with a title line in bold followed by the summary paragraph.
# INPUT:
INPUT:

View File

View File

@@ -0,0 +1,14 @@
# extract_ctf_writeup
<h4><code>extract_ctf_writeup</code> is a <a href="https://github.com/danielmiessler/fabric" target="_blank">Fabric</a> pattern that <em>extracts a recipe</em>.</h4>
## Description
This pattern is used to create a short recipe, consisting of two parts:
- A list of ingredients
- A step by step guide on how to prepare the meal
## Meta
- **Author**: Martin Riedel

View File

@@ -0,0 +1,36 @@
# IDENTITY and PURPOSE
You are a passionate chef. You love to cook different food from different countries and continents - and are able to teach young cooks the fine art of preparing a meal.
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
# STEPS
- Extract a short description of the meal. It should be at most three sentences. Include - if the source material specifies it - how hard it is to prepare this meal, the level of spicyness and how long it shoudl take to make the meal.
- List the INGREDIENTS. Include the measurements.
- List the Steps that are necessary to prepare the meal.
# OUTPUT INSTRUCTIONS
- Only output Markdown.
- Do not give warnings or notes; only output the requested sections.
- You use bulleted lists for output, not numbered lists.
- Do not repeat ideas, quotes, facts, or resources.
- Do not start items with the same opening words.
- Stick to the measurements, do not alter it.
- Ensure you follow ALL these instructions when creating your output.
# INPUT
INPUT:

View File

@@ -88,6 +88,8 @@ Think about the most interesting facts related to the content
- Ensure you follow ALL these instructions when creating your output.
- Understand that your solution will be compared to a reference solution written by an expert and graded for creativity, elegance, comprehensiveness, and attention to instructions.
# INPUT
INPUT:

View File

@@ -1 +1 @@
"1.4.108"
"1.4.121"

View File

@@ -6,9 +6,12 @@ import (
"path/filepath"
"strings"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins/template"
)
const inputSentinel = "__FABRIC_INPUT_SENTINEL_TOKEN__"
type PatternsEntity struct {
*StorageEntity
SystemPatternFile string
@@ -22,50 +25,66 @@ type Pattern struct {
Pattern string
}
// main entry point for getting patterns from any source
func (o *PatternsEntity) GetApplyVariables(source string, variables map[string]string, input string) (*Pattern, error) {
var pattern *Pattern
var err error
// GetApplyVariables main entry point for getting patterns from any source
func (o *PatternsEntity) GetApplyVariables(
source string, variables map[string]string, input string) (pattern *Pattern, err error) {
// Determine if this is a file path
isFilePath := strings.HasPrefix(source, "\\") ||
strings.HasPrefix(source, "/") ||
strings.HasPrefix(source, "~") ||
strings.HasPrefix(source, ".")
if isFilePath {
pattern, err = o.getFromFile(source)
} else {
pattern, err = o.getFromDB(source)
}
// Determine if this is a file path
isFilePath := strings.HasPrefix(source, "\\") ||
strings.HasPrefix(source, "/") ||
strings.HasPrefix(source, "~") ||
strings.HasPrefix(source, ".")
if err != nil {
return nil, err
}
pattern, err = o.applyVariables(pattern, variables, input)
if isFilePath {
// Resolve the file path using GetAbsolutePath
absPath, err := common.GetAbsolutePath(source)
if err != nil {
return nil, err // Return the error if applyVariables failed
return nil, fmt.Errorf("could not resolve file path: %v", err)
}
return pattern, nil
// Use the resolved absolute path to get the pattern
pattern, err = o.getFromFile(absPath)
} else {
// Otherwise, get the pattern from the database
pattern, err = o.getFromDB(source)
}
if err != nil {
return
}
// Apply variables to the pattern
err = o.applyVariables(pattern, variables, input)
return
}
func (o *PatternsEntity) applyVariables(
pattern *Pattern, variables map[string]string, input string) (err error) {
func (o *PatternsEntity) applyVariables(pattern *Pattern, variables map[string]string, input string) (*Pattern, error) {
// If {{input}} isn't in pattern, append it on new line
// Ensure pattern has an {{input}} placeholder
// If not present, append it on a new line
if !strings.Contains(pattern.Pattern, "{{input}}") {
if !strings.HasSuffix(pattern.Pattern, "\n") {
pattern.Pattern += "\n"
}
pattern.Pattern += "{{input}}"
if !strings.HasSuffix(pattern.Pattern, "\n") {
pattern.Pattern += "\n"
}
pattern.Pattern += "{{input}}"
}
result, err := template.ApplyTemplate(pattern.Pattern, variables, input)
if err != nil {
return nil, err
// Temporarily replace {{input}} with a sentinel token to protect it
// from recursive variable resolution
withSentinel := strings.ReplaceAll(pattern.Pattern, "{{input}}", inputSentinel)
// Process all other template variables in the pattern
// At this point, our sentinel ensures {{input}} won't be affected
var processed string
if processed, err = template.ApplyTemplate(withSentinel, variables, ""); err != nil {
return
}
pattern.Pattern = result
return pattern, nil
// Finally, replace our sentinel with the actual user input
// The input has already been processed for variables if InputHasVars was true
pattern.Pattern = strings.ReplaceAll(processed, inputSentinel, input)
return
}
// retrieves a pattern from the database by name
@@ -103,29 +122,31 @@ func (o *PatternsEntity) PrintLatestPatterns(latestNumber int) (err error) {
}
// reads a pattern from a file path and returns it
func (o *PatternsEntity) getFromFile(pathStr string) (*Pattern, error) {
func (o *PatternsEntity) getFromFile(pathStr string) (pattern *Pattern, err error) {
// Handle home directory expansion
if strings.HasPrefix(pathStr, "~/") {
homedir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("could not get home directory: %v", err)
}
pathStr = filepath.Join(homedir, pathStr[2:])
var homedir string
if homedir, err = os.UserHomeDir(); err != nil {
err = fmt.Errorf("could not get home directory: %v", err)
return
}
pathStr = filepath.Join(homedir, pathStr[2:])
}
content, err := os.ReadFile(pathStr)
if err != nil {
return nil, fmt.Errorf("could not read pattern file %s: %v", pathStr, err)
var content []byte
if content, err = os.ReadFile(pathStr); err != nil {
err = fmt.Errorf("could not read pattern file %s: %v", pathStr, err)
return
}
return &Pattern{
Name: pathStr,
Pattern: string(content),
}, nil
pattern = &Pattern{
Name: pathStr,
Pattern: string(content),
}
return
}
// Get required for Storage interface
func (o *PatternsEntity) Get(name string) (*Pattern, error) {
// Use GetPattern with no variables
return o.GetApplyVariables(name, nil, "")
}
}

View File

@@ -43,7 +43,7 @@ func createTestPattern(t *testing.T, entity *PatternsEntity, name, content strin
func TestApplyVariables(t *testing.T) {
entity := &PatternsEntity{}
tests := []struct {
name string
pattern *Pattern
@@ -79,15 +79,15 @@ func TestApplyVariables(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := entity.applyVariables(tt.pattern, tt.variables, tt.input)
err := entity.applyVariables(tt.pattern, tt.variables, tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, result.Pattern)
assert.Equal(t, tt.want, tt.pattern.Pattern)
})
}
}
@@ -117,15 +117,15 @@ func TestGetApplyVariables(t *testing.T) {
want: "You are a reviewer.\ncheck this code",
},
{
name: "pattern with missing variable",
source: "test-pattern",
name: "pattern with missing variable",
source: "test-pattern",
variables: map[string]string{},
input: "test input",
wantErr: true,
},
{
name: "non-existent pattern",
source: "non-existent",
name: "non-existent pattern",
source: "non-existent",
wantErr: true,
},
}
@@ -133,14 +133,14 @@ func TestGetApplyVariables(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := entity.GetApplyVariables(tt.source, tt.variables, tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, result.Pattern)
})
}
}
}

View File

@@ -58,8 +58,7 @@ func (o *Session) Append(messages ...*goopenai.ChatCompletionMessage) {
}
func (o *Session) GetVendorMessages() (ret []*goopenai.ChatCompletionMessage) {
if o.vendorMessages == nil {
o.vendorMessages = []*goopenai.ChatCompletionMessage{}
if len(o.vendorMessages) == 0 {
for _, message := range o.Messages {
o.appendVendorMessage(message)
}

View File

@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/samber/lo"
"github.com/danielmiessler/fabric/common"
)
type StorageEntity struct {
@@ -26,37 +26,44 @@ func (o *StorageEntity) Configure() (err error) {
// GetNames finds all patterns in the patterns directory and enters the id, name, and pattern into a slice of Entry structs. it returns these entries or an error
func (o *StorageEntity) GetNames() (ret []string, err error) {
var entries []os.DirEntry
if entries, err = os.ReadDir(o.Dir); err != nil {
err = fmt.Errorf("could not read items from directory: %v", err)
return
// Resolve the directory path to an absolute path
absDir, err := common.GetAbsolutePath(o.Dir)
if err != nil {
return nil, fmt.Errorf("could not resolve directory path: %v", err)
}
if o.ItemIsDir {
ret = lo.FilterMap(entries, func(item os.DirEntry, index int) (ret string, ok bool) {
if ok = item.IsDir(); ok {
ret = item.Name()
// Read the directory entries
var entries []os.DirEntry
if entries, err = os.ReadDir(absDir); err != nil {
return nil, fmt.Errorf("could not read items from directory: %v", err)
}
for _, entry := range entries {
entryPath := filepath.Join(absDir, entry.Name())
// Get metadata for the entry, including symlink info
fileInfo, err := os.Lstat(entryPath)
if err != nil {
return nil, fmt.Errorf("could not stat entry %s: %v", entryPath, err)
}
// Determine if the entry should be included
if o.ItemIsDir {
// Include directories or symlinks to directories
if fileInfo.IsDir() || (fileInfo.Mode()&os.ModeSymlink != 0 && common.IsSymlinkToDir(entryPath)) {
ret = append(ret, entry.Name())
}
return
})
} else {
if o.FileExtension == "" {
ret = lo.FilterMap(entries, func(item os.DirEntry, index int) (ret string, ok bool) {
if ok = !item.IsDir(); ok {
ret = item.Name()
}
return
})
} else {
ret = lo.FilterMap(entries, func(item os.DirEntry, index int) (ret string, ok bool) {
if ok = !item.IsDir() && filepath.Ext(item.Name()) == o.FileExtension; ok {
ret = strings.TrimSuffix(item.Name(), o.FileExtension)
// Include files, optionally filtering by extension
if !fileInfo.IsDir() {
if o.FileExtension == "" || filepath.Ext(entry.Name()) == o.FileExtension {
ret = append(ret, strings.TrimSuffix(entry.Name(), o.FileExtension))
}
return
})
}
}
}
return
return ret, nil
}
func (o *StorageEntity) Delete(name string) (err error) {

View File

@@ -17,128 +17,128 @@ type DateTimePlugin struct{}
// Period: startofweek, endofweek, startofmonth, endofmonth
// Relative: rel:-1h, rel:-2d, rel:1w, rel:3m, rel:1y
func (p *DateTimePlugin) Apply(operation string, value string) (string, error) {
debugf("DateTime: operation=%q value=%q", operation, value)
now := time.Now()
debugf("DateTime: reference time=%v", now)
switch operation {
// Time operations
case "now":
result := now.Format(time.RFC3339)
debugf("DateTime: now=%q", result)
return result, nil
case "time":
result := now.Format("15:04:05")
debugf("DateTime: time=%q", result)
return result, nil
case "unix":
result := fmt.Sprintf("%d", now.Unix())
debugf("DateTime: unix=%q", result)
return result, nil
case "startofhour":
result := now.Truncate(time.Hour).Format(time.RFC3339)
debugf("DateTime: startofhour=%q", result)
return result, nil
case "endofhour":
result := now.Truncate(time.Hour).Add(time.Hour - time.Second).Format(time.RFC3339)
debugf("DateTime: endofhour=%q", result)
return result, nil
// Date operations
case "today":
result := now.Format("2006-01-02")
debugf("DateTime: today=%q", result)
return result, nil
case "full":
result := now.Format("Monday, January 2, 2006")
debugf("DateTime: full=%q", result)
return result, nil
case "month":
result := now.Format("January")
debugf("DateTime: month=%q", result)
return result, nil
case "year":
result := now.Format("2006")
debugf("DateTime: year=%q", result)
return result, nil
case "startofweek":
result := now.AddDate(0, 0, -int(now.Weekday())).Format("2006-01-02")
debugf("DateTime: startofweek=%q", result)
return result, nil
case "endofweek":
result := now.AddDate(0, 0, 7-int(now.Weekday())).Format("2006-01-02")
debugf("DateTime: endofweek=%q", result)
return result, nil
case "startofmonth":
result := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
debugf("DateTime: startofmonth=%q", result)
return result, nil
case "endofmonth":
result := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
debugf("DateTime: endofmonth=%q", result)
return result, nil
case "rel":
return p.handleRelative(now, value)
default:
return "", fmt.Errorf("datetime: unknown operation %q (see plugin documentation for supported operations)", operation)
}
debugf("DateTime: operation=%q value=%q", operation, value)
now := time.Now()
debugf("DateTime: reference time=%v", now)
switch operation {
// Time operations
case "now":
result := now.Format(time.RFC3339)
debugf("DateTime: now=%q", result)
return result, nil
case "time":
result := now.Format("15:04:05")
debugf("DateTime: time=%q", result)
return result, nil
case "unix":
result := fmt.Sprintf("%d", now.Unix())
debugf("DateTime: unix=%q", result)
return result, nil
case "startofhour":
result := now.Truncate(time.Hour).Format(time.RFC3339)
debugf("DateTime: startofhour=%q", result)
return result, nil
case "endofhour":
result := now.Truncate(time.Hour).Add(time.Hour - time.Second).Format(time.RFC3339)
debugf("DateTime: endofhour=%q", result)
return result, nil
// Date operations
case "today":
result := now.Format("2006-01-02")
debugf("DateTime: today=%q", result)
return result, nil
case "full":
result := now.Format("Monday, January 2, 2006")
debugf("DateTime: full=%q", result)
return result, nil
case "month":
result := now.Format("January")
debugf("DateTime: month=%q", result)
return result, nil
case "year":
result := now.Format("2006")
debugf("DateTime: year=%q", result)
return result, nil
case "startofweek":
result := now.AddDate(0, 0, -int(now.Weekday())).Format("2006-01-02")
debugf("DateTime: startofweek=%q", result)
return result, nil
case "endofweek":
result := now.AddDate(0, 0, 7-int(now.Weekday())).Format("2006-01-02")
debugf("DateTime: endofweek=%q", result)
return result, nil
case "startofmonth":
result := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
debugf("DateTime: startofmonth=%q", result)
return result, nil
case "endofmonth":
result := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
debugf("DateTime: endofmonth=%q", result)
return result, nil
case "rel":
return p.handleRelative(now, value)
default:
return "", fmt.Errorf("datetime: unknown operation %q (see plugin documentation for supported operations)", operation)
}
}
func (p *DateTimePlugin) handleRelative(now time.Time, value string) (string, error) {
debugf("DateTime: handling relative time value=%q", value)
if value == "" {
return "", fmt.Errorf("datetime: relative time requires a value (e.g., -1h, -1d, -1w)")
}
// Try standard duration first (hours, minutes)
if duration, err := time.ParseDuration(value); err == nil {
result := now.Add(duration).Format(time.RFC3339)
debugf("DateTime: relative duration=%q result=%q", duration, result)
return result, nil
}
// Handle date units
if len(value) < 2 {
return "", fmt.Errorf("datetime: invalid relative format (use: -1h, 2d, -3w, 1m, -1y)")
}
unit := value[len(value)-1:]
numStr := value[:len(value)-1]
num, err := strconv.Atoi(numStr)
if err != nil {
return "", fmt.Errorf("datetime: invalid number in relative time: %q", value)
}
var result string
switch unit {
case "d":
result = now.AddDate(0, 0, num).Format("2006-01-02")
case "w":
result = now.AddDate(0, 0, num*7).Format("2006-01-02")
case "m":
result = now.AddDate(0, num, 0).Format("2006-01-02")
case "y":
result = now.AddDate(num, 0, 0).Format("2006-01-02")
default:
return "", fmt.Errorf("datetime: invalid unit %q (use: h,m for time or d,w,m,y for date)", unit)
}
debugf("DateTime: relative unit=%q num=%d result=%q", unit, num, result)
return result, nil
}
debugf("DateTime: handling relative time value=%q", value)
if value == "" {
return "", fmt.Errorf("datetime: relative time requires a value (e.g., -1h, -1d, -1w)")
}
// Try standard duration first (hours, minutes)
if duration, err := time.ParseDuration(value); err == nil {
result := now.Add(duration).Format(time.RFC3339)
debugf("DateTime: relative duration=%q result=%q", duration, result)
return result, nil
}
// Handle date units
if len(value) < 2 {
return "", fmt.Errorf("datetime: invalid relative format (use: -1h, 2d, -3w, 1m, -1y)")
}
unit := value[len(value)-1:]
numStr := value[:len(value)-1]
num, err := strconv.Atoi(numStr)
if err != nil {
return "", fmt.Errorf("datetime: invalid number in relative time: %q", value)
}
var result string
switch unit {
case "d":
result = now.AddDate(0, 0, num).Format("2006-01-02")
case "w":
result = now.AddDate(0, 0, num*7).Format("2006-01-02")
case "m":
result = now.AddDate(0, num, 0).Format("2006-01-02")
case "y":
result = now.AddDate(num, 0, 0).Format("2006-01-02")
default:
return "", fmt.Errorf("datetime: invalid unit %q (use: h,m for time or d,w,m,y for date)", unit)
}
debugf("DateTime: relative unit=%q num=%d result=%q", unit, num, result)
return result, nil
}

View File

@@ -9,130 +9,130 @@ import (
)
func TestDateTimePlugin(t *testing.T) {
plugin := &DateTimePlugin{}
now := time.Now()
tests := []struct {
name string
operation string
value string
validate func(string) error
wantErr bool
}{
{
name: "now returns RFC3339",
operation: "now",
validate: func(got string) error {
if _, err := time.Parse(time.RFC3339, got); err != nil {
return err
}
return nil
},
},
{
name: "time returns HH:MM:SS",
operation: "time",
validate: func(got string) error {
if _, err := time.Parse("15:04:05", got); err != nil {
return err
}
return nil
},
},
{
name: "unix returns timestamp",
operation: "unix",
validate: func(got string) error {
if _, err := strconv.ParseInt(got, 10, 64); err != nil {
return err
}
return nil
},
},
{
name: "today returns YYYY-MM-DD",
operation: "today",
validate: func(got string) error {
if _, err := time.Parse("2006-01-02", got); err != nil {
return err
}
return nil
},
},
{
name: "full returns long date",
operation: "full",
validate: func(got string) error {
if !strings.Contains(got, now.Month().String()) {
return fmt.Errorf("full date missing month name")
}
return nil
},
},
{
name: "relative positive hours",
operation: "rel",
value: "2h",
validate: func(got string) error {
t, err := time.Parse(time.RFC3339, got)
if err != nil {
return err
}
expected := now.Add(2 * time.Hour)
if t.Hour() != expected.Hour() {
return fmt.Errorf("expected hour %d, got %d", expected.Hour(), t.Hour())
}
return nil
},
},
{
name: "relative negative days",
operation: "rel",
value: "-2d",
validate: func(got string) error {
t, err := time.Parse("2006-01-02", got)
if err != nil {
return err
}
expected := now.AddDate(0, 0, -2)
if t.Day() != expected.Day() {
return fmt.Errorf("expected day %d, got %d", expected.Day(), t.Day())
}
return nil
},
},
// Error cases
{
name: "invalid operation",
operation: "invalid",
wantErr: true,
},
{
name: "empty relative value",
operation: "rel",
value: "",
wantErr: true,
},
{
name: "invalid relative format",
operation: "rel",
value: "2x",
wantErr: true,
},
}
plugin := &DateTimePlugin{}
now := time.Now()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("DateTimePlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && tt.validate != nil {
if err := tt.validate(got); err != nil {
t.Errorf("DateTimePlugin.Apply() validation failed: %v", err)
}
}
})
}
}
tests := []struct {
name string
operation string
value string
validate func(string) error
wantErr bool
}{
{
name: "now returns RFC3339",
operation: "now",
validate: func(got string) error {
if _, err := time.Parse(time.RFC3339, got); err != nil {
return err
}
return nil
},
},
{
name: "time returns HH:MM:SS",
operation: "time",
validate: func(got string) error {
if _, err := time.Parse("15:04:05", got); err != nil {
return err
}
return nil
},
},
{
name: "unix returns timestamp",
operation: "unix",
validate: func(got string) error {
if _, err := strconv.ParseInt(got, 10, 64); err != nil {
return err
}
return nil
},
},
{
name: "today returns YYYY-MM-DD",
operation: "today",
validate: func(got string) error {
if _, err := time.Parse("2006-01-02", got); err != nil {
return err
}
return nil
},
},
{
name: "full returns long date",
operation: "full",
validate: func(got string) error {
if !strings.Contains(got, now.Month().String()) {
return fmt.Errorf("full date missing month name")
}
return nil
},
},
{
name: "relative positive hours",
operation: "rel",
value: "2h",
validate: func(got string) error {
t, err := time.Parse(time.RFC3339, got)
if err != nil {
return err
}
expected := now.Add(2 * time.Hour)
if t.Hour() != expected.Hour() {
return fmt.Errorf("expected hour %d, got %d", expected.Hour(), t.Hour())
}
return nil
},
},
{
name: "relative negative days",
operation: "rel",
value: "-2d",
validate: func(got string) error {
t, err := time.Parse("2006-01-02", got)
if err != nil {
return err
}
expected := now.AddDate(0, 0, -2)
if t.Day() != expected.Day() {
return fmt.Errorf("expected day %d, got %d", expected.Day(), t.Day())
}
return nil
},
},
// Error cases
{
name: "invalid operation",
operation: "invalid",
wantErr: true,
},
{
name: "empty relative value",
operation: "rel",
value: "",
wantErr: true,
},
{
name: "invalid relative format",
operation: "rel",
value: "2x",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("DateTimePlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && tt.validate != nil {
if err := tt.validate(got); err != nil {
t.Errorf("DateTimePlugin.Apply() validation failed: %v", err)
}
}
})
}
}

Binary file not shown.

View File

@@ -14,11 +14,11 @@ import (
)
const (
// MaxContentSize limits response size to 1MB to prevent memory issues
MaxContentSize = 1024 * 1024
// UserAgent identifies the client in HTTP requests
UserAgent = "Fabric-Fetch/1.0"
// MaxContentSize limits response size to 1MB to prevent memory issues
MaxContentSize = 1024 * 1024
// UserAgent identifies the client in HTTP requests
UserAgent = "Fabric-Fetch/1.0"
)
// FetchPlugin provides HTTP fetching capabilities with safety constraints:
@@ -31,104 +31,104 @@ type FetchPlugin struct{}
// Apply executes fetch operations:
// - get:URL: Fetches content from URL, returns text content
func (p *FetchPlugin) Apply(operation string, value string) (string, error) {
debugf("Fetch: operation=%q value=%q", operation, value)
switch operation {
case "get":
return p.fetch(value)
default:
return "", fmt.Errorf("fetch: unknown operation %q (supported: get)", operation)
}
debugf("Fetch: operation=%q value=%q", operation, value)
switch operation {
case "get":
return p.fetch(value)
default:
return "", fmt.Errorf("fetch: unknown operation %q (supported: get)", operation)
}
}
// isTextContent checks if the content type is text-based
func (p *FetchPlugin) isTextContent(contentType string) bool {
debugf("Fetch: checking content type %q", contentType)
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
debugf("Fetch: error parsing media type: %v", err)
return false
}
isText := strings.HasPrefix(mediaType, "text/") ||
mediaType == "application/json" ||
mediaType == "application/xml" ||
mediaType == "application/yaml" ||
mediaType == "application/x-yaml" ||
strings.HasSuffix(mediaType, "+json") ||
strings.HasSuffix(mediaType, "+xml") ||
strings.HasSuffix(mediaType, "+yaml")
debugf("Fetch: content type %q is text: %v", mediaType, isText)
return isText
debugf("Fetch: checking content type %q", contentType)
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
debugf("Fetch: error parsing media type: %v", err)
return false
}
isText := strings.HasPrefix(mediaType, "text/") ||
mediaType == "application/json" ||
mediaType == "application/xml" ||
mediaType == "application/yaml" ||
mediaType == "application/x-yaml" ||
strings.HasSuffix(mediaType, "+json") ||
strings.HasSuffix(mediaType, "+xml") ||
strings.HasSuffix(mediaType, "+yaml")
debugf("Fetch: content type %q is text: %v", mediaType, isText)
return isText
}
// validateTextContent ensures content is valid UTF-8 without null bytes
func (p *FetchPlugin) validateTextContent(content []byte) error {
debugf("Fetch: validating content length=%d bytes", len(content))
if !utf8.Valid(content) {
return fmt.Errorf("fetch: content is not valid UTF-8 text")
}
if bytes.Contains(content, []byte{0}) {
return fmt.Errorf("fetch: content contains null bytes")
}
debugf("Fetch: content validation successful")
return nil
debugf("Fetch: validating content length=%d bytes", len(content))
if !utf8.Valid(content) {
return fmt.Errorf("fetch: content is not valid UTF-8 text")
}
if bytes.Contains(content, []byte{0}) {
return fmt.Errorf("fetch: content contains null bytes")
}
debugf("Fetch: content validation successful")
return nil
}
// fetch retrieves content from a URL with safety checks
func (p *FetchPlugin) fetch(urlStr string) (string, error) {
debugf("Fetch: requesting URL %q", urlStr)
client := &http.Client{}
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return "", fmt.Errorf("fetch: error creating request: %v", err)
}
req.Header.Set("User-Agent", UserAgent)
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("fetch: error fetching URL: %v", err)
}
defer resp.Body.Close()
debugf("Fetch: got response status=%q", resp.Status)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("fetch: HTTP error: %d - %s", resp.StatusCode, resp.Status)
}
debugf("Fetch: requesting URL %q", urlStr)
if contentLength := resp.ContentLength; contentLength > MaxContentSize {
return "", fmt.Errorf("fetch: content too large: %d bytes (max %d bytes)",
contentLength, MaxContentSize)
}
contentType := resp.Header.Get("Content-Type")
debugf("Fetch: content-type=%q", contentType)
if !p.isTextContent(contentType) {
return "", fmt.Errorf("fetch: unsupported content type %q - only text content allowed",
contentType)
}
client := &http.Client{}
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return "", fmt.Errorf("fetch: error creating request: %v", err)
}
req.Header.Set("User-Agent", UserAgent)
debugf("Fetch: reading response body")
limitReader := io.LimitReader(resp.Body, MaxContentSize+1)
content, err := io.ReadAll(limitReader)
if err != nil {
return "", fmt.Errorf("fetch: error reading response: %v", err)
}
if len(content) > MaxContentSize {
return "", fmt.Errorf("fetch: content too large: exceeds %d bytes", MaxContentSize)
}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("fetch: error fetching URL: %v", err)
}
defer resp.Body.Close()
if err := p.validateTextContent(content); err != nil {
return "", err
}
debugf("Fetch: got response status=%q", resp.Status)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("fetch: HTTP error: %d - %s", resp.StatusCode, resp.Status)
}
debugf("Fetch: operation completed successfully, read %d bytes", len(content))
return string(content), nil
}
if contentLength := resp.ContentLength; contentLength > MaxContentSize {
return "", fmt.Errorf("fetch: content too large: %d bytes (max %d bytes)",
contentLength, MaxContentSize)
}
contentType := resp.Header.Get("Content-Type")
debugf("Fetch: content-type=%q", contentType)
if !p.isTextContent(contentType) {
return "", fmt.Errorf("fetch: unsupported content type %q - only text content allowed",
contentType)
}
debugf("Fetch: reading response body")
limitReader := io.LimitReader(resp.Body, MaxContentSize+1)
content, err := io.ReadAll(limitReader)
if err != nil {
return "", fmt.Errorf("fetch: error reading response: %v", err)
}
if len(content) > MaxContentSize {
return "", fmt.Errorf("fetch: content too large: exceeds %d bytes", MaxContentSize)
}
if err := p.validateTextContent(content); err != nil {
return "", err
}
debugf("Fetch: operation completed successfully, read %d bytes", len(content))
return string(content), nil
}

View File

@@ -5,67 +5,68 @@ import (
"strings"
"testing"
)
func TestFetchPlugin(t *testing.T) {
plugin := &FetchPlugin{}
tests := []struct {
name string
operation string
value string
server func() *httptest.Server
wantErr bool
errContains string
name string
operation string
value string
server func() *httptest.Server
wantErr bool
errContains string
}{
// ... keep existing valid test cases ...
{
name: "invalid URL",
operation: "get",
value: "not-a-url",
wantErr: true,
errContains: "unsupported protocol", // Updated to match actual error
},
{
name: "malformed URL",
operation: "get",
value: "http://[::1]:namedport",
wantErr: true,
errContains: "error creating request",
},
// ... keep other test cases ...
// ... keep existing valid test cases ...
{
name: "invalid URL",
operation: "get",
value: "not-a-url",
wantErr: true,
errContains: "unsupported protocol", // Updated to match actual error
},
{
name: "malformed URL",
operation: "get",
value: "http://[::1]:namedport",
wantErr: true,
errContains: "error creating request",
},
// ... keep other test cases ...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var url string
if tt.server != nil {
server := tt.server()
defer server.Close()
url = server.URL
} else {
url = tt.value
}
got, err := plugin.Apply(tt.operation, url)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("FetchPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
t.Logf("Full error: %v", err) // Added for better debugging
}
return
}
// For successful cases, verify we got some content
if err == nil && got == "" {
t.Error("FetchPlugin.Apply() returned empty content on success")
}
})
t.Run(tt.name, func(t *testing.T) {
var url string
if tt.server != nil {
server := tt.server()
defer server.Close()
url = server.URL
} else {
url = tt.value
}
got, err := plugin.Apply(tt.operation, url)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("FetchPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
t.Logf("Full error: %v", err) // Added for better debugging
}
return
}
// For successful cases, verify we got some content
if err == nil && got == "" {
t.Error("FetchPlugin.Apply() returned empty content on success")
}
})
}
}
}

View File

@@ -24,26 +24,26 @@ type FilePlugin struct{}
// safePath validates and normalizes file paths
func (p *FilePlugin) safePath(path string) (string, error) {
debugf("File: validating path %q", path)
// Basic security check - no path traversal
if strings.Contains(path, "..") {
return "", fmt.Errorf("file: path cannot contain '..'")
}
// Expand home directory if needed
if strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("file: could not expand home directory: %v", err)
}
path = filepath.Join(home, path[2:])
}
// Clean the path
cleaned := filepath.Clean(path)
debugf("File: cleaned path %q", cleaned)
return cleaned, nil
debugf("File: validating path %q", path)
// Basic security check - no path traversal
if strings.Contains(path, "..") {
return "", fmt.Errorf("file: path cannot contain '..'")
}
// Expand home directory if needed
if strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("file: could not expand home directory: %v", err)
}
path = filepath.Join(home, path[2:])
}
// Clean the path
cleaned := filepath.Clean(path)
debugf("File: cleaned path %q", cleaned)
return cleaned, nil
}
// Apply executes file operations:
@@ -53,145 +53,145 @@ func (p *FilePlugin) safePath(path string) (string, error) {
// - size:PATH - Get file size in bytes
// - modified:PATH - Get last modified time
func (p *FilePlugin) Apply(operation string, value string) (string, error) {
debugf("File: operation=%q value=%q", operation, value)
switch operation {
case "tail":
parts := strings.Split(value, "|")
if len(parts) != 2 {
return "", fmt.Errorf("file: tail requires format path|lines")
}
path, err := p.safePath(parts[0])
if err != nil {
return "", err
}
n, err := strconv.Atoi(parts[1])
if err != nil {
return "", fmt.Errorf("file: invalid line count %q", parts[1])
}
if n < 1 {
return "", fmt.Errorf("file: line count must be positive")
}
lines, err := p.lastNLines(path, n)
if err != nil {
return "", err
}
result := strings.Join(lines, "\n")
debugf("File: tail returning %d lines", len(lines))
return result, nil
debugf("File: operation=%q value=%q", operation, value)
case "read":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
if info.Size() > MaxFileSize {
return "", fmt.Errorf("file: size %d exceeds limit of %d bytes",
info.Size(), MaxFileSize)
}
content, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("file: could not read: %v", err)
}
debugf("File: read %d bytes", len(content))
return string(content), nil
switch operation {
case "tail":
parts := strings.Split(value, "|")
if len(parts) != 2 {
return "", fmt.Errorf("file: tail requires format path|lines")
}
case "exists":
path, err := p.safePath(value)
if err != nil {
return "", err
}
_, err = os.Stat(path)
exists := err == nil
debugf("File: exists=%v for path %q", exists, path)
return fmt.Sprintf("%t", exists), nil
path, err := p.safePath(parts[0])
if err != nil {
return "", err
}
case "size":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
size := info.Size()
debugf("File: size=%d for path %q", size, path)
return fmt.Sprintf("%d", size), nil
n, err := strconv.Atoi(parts[1])
if err != nil {
return "", fmt.Errorf("file: invalid line count %q", parts[1])
}
case "modified":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
mtime := info.ModTime().Format(time.RFC3339)
debugf("File: modified=%q for path %q", mtime, path)
return mtime, nil
if n < 1 {
return "", fmt.Errorf("file: line count must be positive")
}
default:
return "", fmt.Errorf("file: unknown operation %q (supported: read, tail, exists, size, modified)",
operation)
}
lines, err := p.lastNLines(path, n)
if err != nil {
return "", err
}
result := strings.Join(lines, "\n")
debugf("File: tail returning %d lines", len(lines))
return result, nil
case "read":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
if info.Size() > MaxFileSize {
return "", fmt.Errorf("file: size %d exceeds limit of %d bytes",
info.Size(), MaxFileSize)
}
content, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("file: could not read: %v", err)
}
debugf("File: read %d bytes", len(content))
return string(content), nil
case "exists":
path, err := p.safePath(value)
if err != nil {
return "", err
}
_, err = os.Stat(path)
exists := err == nil
debugf("File: exists=%v for path %q", exists, path)
return fmt.Sprintf("%t", exists), nil
case "size":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
size := info.Size()
debugf("File: size=%d for path %q", size, path)
return fmt.Sprintf("%d", size), nil
case "modified":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
mtime := info.ModTime().Format(time.RFC3339)
debugf("File: modified=%q for path %q", mtime, path)
return mtime, nil
default:
return "", fmt.Errorf("file: unknown operation %q (supported: read, tail, exists, size, modified)",
operation)
}
}
// lastNLines returns the last n lines from a file
func (p *FilePlugin) lastNLines(path string, n int) ([]string, error) {
debugf("File: reading last %d lines from %q", n, path)
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("file: could not open: %v", err)
}
defer file.Close()
debugf("File: reading last %d lines from %q", n, path)
info, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("file: could not stat: %v", err)
}
if info.Size() > MaxFileSize {
return nil, fmt.Errorf("file: size %d exceeds limit of %d bytes",
info.Size(), MaxFileSize)
}
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("file: could not open: %v", err)
}
defer file.Close()
lines := make([]string, 0, n)
scanner := bufio.NewScanner(file)
lineCount := 0
for scanner.Scan() {
lineCount++
if len(lines) == n {
lines = lines[1:]
}
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("file: error reading: %v", err)
}
info, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("file: could not stat: %v", err)
}
debugf("File: read %d lines total, returning last %d", lineCount, len(lines))
return lines, nil
if info.Size() > MaxFileSize {
return nil, fmt.Errorf("file: size %d exceeds limit of %d bytes",
info.Size(), MaxFileSize)
}
lines := make([]string, 0, n)
scanner := bufio.NewScanner(file)
lineCount := 0
for scanner.Scan() {
lineCount++
if len(lines) == n {
lines = lines[1:]
}
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("file: error reading: %v", err)
}
debugf("File: read %d lines total, returning last %d", lineCount, len(lines))
return lines, nil
}

View File

@@ -9,144 +9,144 @@ import (
)
func TestFilePlugin(t *testing.T) {
plugin := &FilePlugin{}
// Create temp test files
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
content := "line1\nline2\nline3\nline4\nline5\n"
err := os.WriteFile(testFile, []byte(content), 0644)
if err != nil {
t.Fatal(err)
}
bigFile := filepath.Join(tmpDir, "big.txt")
err = os.WriteFile(bigFile, []byte(strings.Repeat("x", MaxFileSize+1)), 0644)
if err != nil {
t.Fatal(err)
}
plugin := &FilePlugin{}
tests := []struct {
name string
operation string
value string
want string
wantErr bool
errContains string
validate func(string) bool
}{
{
name: "read file",
operation: "read",
value: testFile,
want: content,
},
{
name: "tail file",
operation: "tail",
value: testFile + "|3",
want: "line3\nline4\nline5",
},
{
name: "exists true",
operation: "exists",
value: testFile,
want: "true",
},
{
name: "exists false",
operation: "exists",
value: filepath.Join(tmpDir, "nonexistent.txt"),
want: "false",
},
{
name: "size",
operation: "size",
value: testFile,
want: "30",
},
{
name: "modified",
operation: "modified",
value: testFile,
validate: func(got string) bool {
_, err := time.Parse(time.RFC3339, got)
return err == nil
},
},
// Error cases
{
name: "read non-existent",
operation: "read",
value: filepath.Join(tmpDir, "nonexistent.txt"),
wantErr: true,
errContains: "could not stat file",
},
{
name: "invalid operation",
operation: "invalid",
value: testFile,
wantErr: true,
errContains: "unknown operation",
},
{
name: "path traversal attempt",
operation: "read",
value: "../../../etc/passwd",
wantErr: true,
errContains: "cannot contain '..'",
},
{
name: "file too large",
operation: "read",
value: bigFile,
wantErr: true,
errContains: "exceeds limit",
},
{
name: "invalid tail format",
operation: "tail",
value: testFile,
wantErr: true,
errContains: "requires format path|lines",
},
{
name: "invalid tail count",
operation: "tail",
value: testFile + "|invalid",
wantErr: true,
errContains: "invalid line count",
},
}
// Create temp test files
tmpDir := t.TempDir()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("FilePlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
}
return
}
// Check success cases
if err == nil {
if tt.validate != nil {
if !tt.validate(got) {
t.Errorf("FilePlugin.Apply() returned invalid result: %q", got)
}
} else if tt.want != "" && got != tt.want {
t.Errorf("FilePlugin.Apply() = %v, want %v", got, tt.want)
}
}
})
}
}
testFile := filepath.Join(tmpDir, "test.txt")
content := "line1\nline2\nline3\nline4\nline5\n"
err := os.WriteFile(testFile, []byte(content), 0644)
if err != nil {
t.Fatal(err)
}
bigFile := filepath.Join(tmpDir, "big.txt")
err = os.WriteFile(bigFile, []byte(strings.Repeat("x", MaxFileSize+1)), 0644)
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
operation string
value string
want string
wantErr bool
errContains string
validate func(string) bool
}{
{
name: "read file",
operation: "read",
value: testFile,
want: content,
},
{
name: "tail file",
operation: "tail",
value: testFile + "|3",
want: "line3\nline4\nline5",
},
{
name: "exists true",
operation: "exists",
value: testFile,
want: "true",
},
{
name: "exists false",
operation: "exists",
value: filepath.Join(tmpDir, "nonexistent.txt"),
want: "false",
},
{
name: "size",
operation: "size",
value: testFile,
want: "30",
},
{
name: "modified",
operation: "modified",
value: testFile,
validate: func(got string) bool {
_, err := time.Parse(time.RFC3339, got)
return err == nil
},
},
// Error cases
{
name: "read non-existent",
operation: "read",
value: filepath.Join(tmpDir, "nonexistent.txt"),
wantErr: true,
errContains: "could not stat file",
},
{
name: "invalid operation",
operation: "invalid",
value: testFile,
wantErr: true,
errContains: "unknown operation",
},
{
name: "path traversal attempt",
operation: "read",
value: "../../../etc/passwd",
wantErr: true,
errContains: "cannot contain '..'",
},
{
name: "file too large",
operation: "read",
value: bigFile,
wantErr: true,
errContains: "exceeds limit",
},
{
name: "invalid tail format",
operation: "tail",
value: testFile,
wantErr: true,
errContains: "requires format path|lines",
},
{
name: "invalid tail count",
operation: "tail",
value: testFile + "|invalid",
wantErr: true,
errContains: "invalid line count",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("FilePlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
}
return
}
// Check success cases
if err == nil {
if tt.validate != nil {
if !tt.validate(got) {
t.Errorf("FilePlugin.Apply() returned invalid result: %q", got)
}
} else if tt.want != "" && got != tt.want {
t.Errorf("FilePlugin.Apply() = %v, want %v", got, tt.want)
}
}
})
}
}

View File

@@ -22,66 +22,66 @@ type SysPlugin struct{}
// - pwd: Current working directory
// - home: User's home directory
func (p *SysPlugin) Apply(operation string, value string) (string, error) {
debugf("Sys: operation=%q value=%q", operation, value)
switch operation {
case "hostname":
hostname, err := os.Hostname()
if err != nil {
debugf("Sys: hostname error: %v", err)
return "", fmt.Errorf("sys: hostname error: %v", err)
}
debugf("Sys: hostname=%q", hostname)
return hostname, nil
case "user":
currentUser, err := user.Current()
if err != nil {
debugf("Sys: user error: %v", err)
return "", fmt.Errorf("sys: user error: %v", err)
}
debugf("Sys: user=%q", currentUser.Username)
return currentUser.Username, nil
case "os":
result := runtime.GOOS
debugf("Sys: os=%q", result)
return result, nil
case "arch":
result := runtime.GOARCH
debugf("Sys: arch=%q", result)
return result, nil
case "env":
if value == "" {
debugf("Sys: env error: missing variable name")
return "", fmt.Errorf("sys: env operation requires a variable name")
}
result := os.Getenv(value)
debugf("Sys: env %q=%q", value, result)
return result, nil
case "pwd":
dir, err := os.Getwd()
if err != nil {
debugf("Sys: pwd error: %v", err)
return "", fmt.Errorf("sys: pwd error: %v", err)
}
debugf("Sys: pwd=%q", dir)
return dir, nil
case "home":
homeDir, err := os.UserHomeDir()
if err != nil {
debugf("Sys: home error: %v", err)
return "", fmt.Errorf("sys: home error: %v", err)
}
debugf("Sys: home=%q", homeDir)
return homeDir, nil
debugf("Sys: operation=%q value=%q", operation, value)
default:
debugf("Sys: unknown operation %q", operation)
return "", fmt.Errorf("sys: unknown operation %q (supported: hostname, user, os, arch, env, pwd, home)", operation)
}
}
switch operation {
case "hostname":
hostname, err := os.Hostname()
if err != nil {
debugf("Sys: hostname error: %v", err)
return "", fmt.Errorf("sys: hostname error: %v", err)
}
debugf("Sys: hostname=%q", hostname)
return hostname, nil
case "user":
currentUser, err := user.Current()
if err != nil {
debugf("Sys: user error: %v", err)
return "", fmt.Errorf("sys: user error: %v", err)
}
debugf("Sys: user=%q", currentUser.Username)
return currentUser.Username, nil
case "os":
result := runtime.GOOS
debugf("Sys: os=%q", result)
return result, nil
case "arch":
result := runtime.GOARCH
debugf("Sys: arch=%q", result)
return result, nil
case "env":
if value == "" {
debugf("Sys: env error: missing variable name")
return "", fmt.Errorf("sys: env operation requires a variable name")
}
result := os.Getenv(value)
debugf("Sys: env %q=%q", value, result)
return result, nil
case "pwd":
dir, err := os.Getwd()
if err != nil {
debugf("Sys: pwd error: %v", err)
return "", fmt.Errorf("sys: pwd error: %v", err)
}
debugf("Sys: pwd=%q", dir)
return dir, nil
case "home":
homeDir, err := os.UserHomeDir()
if err != nil {
debugf("Sys: home error: %v", err)
return "", fmt.Errorf("sys: home error: %v", err)
}
debugf("Sys: home=%q", homeDir)
return homeDir, nil
default:
debugf("Sys: unknown operation %q", operation)
return "", fmt.Errorf("sys: unknown operation %q (supported: hostname, user, os, arch, env, pwd, home)", operation)
}
}

View File

@@ -10,131 +10,131 @@ import (
)
func TestSysPlugin(t *testing.T) {
plugin := &SysPlugin{}
// Set up test environment variable
const testEnvVar = "FABRIC_TEST_VAR"
const testEnvValue = "test_value"
os.Setenv(testEnvVar, testEnvValue)
defer os.Unsetenv(testEnvVar)
tests := []struct {
name string
operation string
value string
validate func(string) error
wantErr bool
}{
{
name: "hostname returns valid name",
operation: "hostname",
validate: func(got string) error {
if got == "" {
return fmt.Errorf("hostname is empty")
}
return nil
},
},
{
name: "user returns current user",
operation: "user",
validate: func(got string) error {
if got == "" {
return fmt.Errorf("username is empty")
}
return nil
},
},
{
name: "os returns valid OS",
operation: "os",
validate: func(got string) error {
if got != runtime.GOOS {
return fmt.Errorf("expected OS %s, got %s", runtime.GOOS, got)
}
return nil
},
},
{
name: "arch returns valid architecture",
operation: "arch",
validate: func(got string) error {
if got != runtime.GOARCH {
return fmt.Errorf("expected arch %s, got %s", runtime.GOARCH, got)
}
return nil
},
},
{
name: "env returns environment variable",
operation: "env",
value: testEnvVar,
validate: func(got string) error {
if got != testEnvValue {
return fmt.Errorf("expected env var %s, got %s", testEnvValue, got)
}
return nil
},
},
{
name: "pwd returns valid directory",
operation: "pwd",
validate: func(got string) error {
if !filepath.IsAbs(got) {
return fmt.Errorf("expected absolute path, got %s", got)
}
return nil
},
},
{
name: "home returns valid home directory",
operation: "home",
validate: func(got string) error {
if !filepath.IsAbs(got) {
return fmt.Errorf("expected absolute path, got %s", got)
}
if !strings.Contains(got, "home") && !strings.Contains(got, "Users") {
return fmt.Errorf("path %s doesn't look like a home directory", got)
}
return nil
},
},
// Error cases
{
name: "unknown operation",
operation: "invalid",
wantErr: true,
},
{
name: "env without variable",
operation: "env",
wantErr: true,
},
{
name: "env with non-existent variable",
operation: "env",
value: "NONEXISTENT_VAR_123456",
validate: func(got string) error {
if got != "" {
return fmt.Errorf("expected empty string for non-existent env var, got %s", got)
}
return nil
},
},
}
plugin := &SysPlugin{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("SysPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && tt.validate != nil {
if err := tt.validate(got); err != nil {
t.Errorf("SysPlugin.Apply() validation failed: %v", err)
}
}
})
}
}
// Set up test environment variable
const testEnvVar = "FABRIC_TEST_VAR"
const testEnvValue = "test_value"
os.Setenv(testEnvVar, testEnvValue)
defer os.Unsetenv(testEnvVar)
tests := []struct {
name string
operation string
value string
validate func(string) error
wantErr bool
}{
{
name: "hostname returns valid name",
operation: "hostname",
validate: func(got string) error {
if got == "" {
return fmt.Errorf("hostname is empty")
}
return nil
},
},
{
name: "user returns current user",
operation: "user",
validate: func(got string) error {
if got == "" {
return fmt.Errorf("username is empty")
}
return nil
},
},
{
name: "os returns valid OS",
operation: "os",
validate: func(got string) error {
if got != runtime.GOOS {
return fmt.Errorf("expected OS %s, got %s", runtime.GOOS, got)
}
return nil
},
},
{
name: "arch returns valid architecture",
operation: "arch",
validate: func(got string) error {
if got != runtime.GOARCH {
return fmt.Errorf("expected arch %s, got %s", runtime.GOARCH, got)
}
return nil
},
},
{
name: "env returns environment variable",
operation: "env",
value: testEnvVar,
validate: func(got string) error {
if got != testEnvValue {
return fmt.Errorf("expected env var %s, got %s", testEnvValue, got)
}
return nil
},
},
{
name: "pwd returns valid directory",
operation: "pwd",
validate: func(got string) error {
if !filepath.IsAbs(got) {
return fmt.Errorf("expected absolute path, got %s", got)
}
return nil
},
},
{
name: "home returns valid home directory",
operation: "home",
validate: func(got string) error {
if !filepath.IsAbs(got) {
return fmt.Errorf("expected absolute path, got %s", got)
}
if !strings.Contains(got, "home") && !strings.Contains(got, "Users") {
return fmt.Errorf("path %s doesn't look like a home directory", got)
}
return nil
},
},
// Error cases
{
name: "unknown operation",
operation: "invalid",
wantErr: true,
},
{
name: "env without variable",
operation: "env",
wantErr: true,
},
{
name: "env with non-existent variable",
operation: "env",
value: "NONEXISTENT_VAR_123456",
validate: func(got string) error {
if got != "" {
return fmt.Errorf("expected empty string for non-existent env var, got %s", got)
}
return nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("SysPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && tt.validate != nil {
if err := tt.validate(got); err != nil {
t.Errorf("SysPlugin.Apply() validation failed: %v", err)
}
}
})
}
}

View File

@@ -7,113 +7,113 @@ import (
)
var (
textPlugin = &TextPlugin{}
datetimePlugin = &DateTimePlugin{}
filePlugin = &FilePlugin{}
fetchPlugin = &FetchPlugin{}
sysPlugin = &SysPlugin{}
Debug = false // Debug flag
textPlugin = &TextPlugin{}
datetimePlugin = &DateTimePlugin{}
filePlugin = &FilePlugin{}
fetchPlugin = &FetchPlugin{}
sysPlugin = &SysPlugin{}
Debug = false // Debug flag
)
var pluginPattern = regexp.MustCompile(`\{\{plugin:([^:]+):([^:]+)(?::([^}]+))?\}\}`)
func debugf(format string, a ...interface{}) {
if Debug {
fmt.Printf(format, a...)
}
if Debug {
fmt.Printf(format, a...)
}
}
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
}
}
// 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")
}
}
}
var missingVars []string
r := regexp.MustCompile(`\{\{([^{}]+)\}\}`)
debugf("Template processing complete\n")
return content, nil
}
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
}
}
// 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("Template processing complete\n")
return content, nil
}

View File

@@ -6,141 +6,140 @@ import (
)
func TestApplyTemplate(t *testing.T) {
tests := []struct {
name string
template string
vars map[string]string
input string
want string
wantErr bool
errContains string
}{
// Basic variable substitution
{
name: "simple variable",
template: "Hello {{name}}!",
vars: map[string]string{"name": "World"},
want: "Hello World!",
},
{
name: "multiple variables",
template: "{{greeting}} {{name}}!",
vars: map[string]string{
"greeting": "Hello",
"name": "World",
},
want: "Hello World!",
},
{
name: "special input variable",
template: "Content: {{input}}",
input: "test content",
want: "Content: test content",
},
// Nested variable substitution
{
name: "nested variables",
template: "{{outer{{inner}}}}",
vars: map[string]string{
"inner": "foo", // First resolution
"outerfoo": "result", // Second resolution
},
want: "result",
},
// Plugin operations
{
name: "simple text plugin",
template: "{{plugin:text:upper:hello}}",
want: "HELLO",
},
{
name: "text plugin with variable",
template: "{{plugin:text:upper:{{name}}}}",
vars: map[string]string{"name": "world"},
want: "WORLD",
},
{
name: "plugin with dynamic operation",
template: "{{plugin:text:{{operation}}:hello}}",
vars: map[string]string{"operation": "upper"},
want: "HELLO",
},
// Multiple operations
{
name: "multiple plugins",
template: "A:{{plugin:text:upper:hello}} B:{{plugin:text:lower:WORLD}}",
want: "A:HELLO B:world",
},
{
name: "nested plugins",
template: "{{plugin:text:upper:{{plugin:text:lower:HELLO}}}}",
want: "HELLO",
},
// Error cases
{
name: "missing variable",
template: "Hello {{name}}!",
wantErr: true,
errContains: "missing required variable",
},
{
name: "unknown plugin",
template: "{{plugin:invalid:op:value}}",
wantErr: true,
errContains: "unknown plugin namespace",
},
{
name: "unknown plugin operation",
template: "{{plugin:text:invalid:value}}",
wantErr: true,
errContains: "unknown text operation",
},
{
name: "nested plugin error",
template: "{{plugin:text:upper:{{plugin:invalid:op:value}}}}",
wantErr: true,
errContains: "unknown plugin namespace",
},
// Edge cases
{
name: "empty template",
template: "",
want: "",
},
{
name: "no substitutions needed",
template: "plain text",
want: "plain text",
},
}
tests := []struct {
name string
template string
vars map[string]string
input string
want string
wantErr bool
errContains string
}{
// Basic variable substitution
{
name: "simple variable",
template: "Hello {{name}}!",
vars: map[string]string{"name": "World"},
want: "Hello World!",
},
{
name: "multiple variables",
template: "{{greeting}} {{name}}!",
vars: map[string]string{
"greeting": "Hello",
"name": "World",
},
want: "Hello World!",
},
{
name: "special input variable",
template: "Content: {{input}}",
input: "test content",
want: "Content: test content",
},
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ApplyTemplate(tt.template, tt.vars, tt.input)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("ApplyTemplate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
}
return
}
// Check result
if got != tt.want {
t.Errorf("ApplyTemplate() = %q, want %q", got, tt.want)
}
})
}
// Nested variable substitution
{
name: "nested variables",
template: "{{outer{{inner}}}}",
vars: map[string]string{
"inner": "foo", // First resolution
"outerfoo": "result", // Second resolution
},
want: "result",
},
// Plugin operations
{
name: "simple text plugin",
template: "{{plugin:text:upper:hello}}",
want: "HELLO",
},
{
name: "text plugin with variable",
template: "{{plugin:text:upper:{{name}}}}",
vars: map[string]string{"name": "world"},
want: "WORLD",
},
{
name: "plugin with dynamic operation",
template: "{{plugin:text:{{operation}}:hello}}",
vars: map[string]string{"operation": "upper"},
want: "HELLO",
},
// Multiple operations
{
name: "multiple plugins",
template: "A:{{plugin:text:upper:hello}} B:{{plugin:text:lower:WORLD}}",
want: "A:HELLO B:world",
},
{
name: "nested plugins",
template: "{{plugin:text:upper:{{plugin:text:lower:HELLO}}}}",
want: "HELLO",
},
// Error cases
{
name: "missing variable",
template: "Hello {{name}}!",
wantErr: true,
errContains: "missing required variable",
},
{
name: "unknown plugin",
template: "{{plugin:invalid:op:value}}",
wantErr: true,
errContains: "unknown plugin namespace",
},
{
name: "unknown plugin operation",
template: "{{plugin:text:invalid:value}}",
wantErr: true,
errContains: "unknown text operation",
},
{
name: "nested plugin error",
template: "{{plugin:text:upper:{{plugin:invalid:op:value}}}}",
wantErr: true,
errContains: "unknown plugin namespace",
},
// Edge cases
{
name: "empty template",
template: "",
want: "",
},
{
name: "no substitutions needed",
template: "plain text",
want: "plain text",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ApplyTemplate(tt.template, tt.vars, tt.input)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("ApplyTemplate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
}
return
}
// Check result
if got != tt.want {
t.Errorf("ApplyTemplate() = %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -12,53 +12,53 @@ type TextPlugin struct{}
// toTitle capitalizes a letter if it follows a non-letter, unless next char is space
func toTitle(s string) string {
// First lowercase everything
lower := strings.ToLower(s)
runes := []rune(lower)
for i := 0; i < len(runes); i++ {
// Capitalize if previous char is non-letter AND
// (we're at the end OR next char is not space)
if (i == 0 || !unicode.IsLetter(runes[i-1])) {
if i == len(runes)-1 || !unicode.IsSpace(runes[i+1]) {
runes[i] = unicode.ToUpper(runes[i])
}
}
}
return string(runes)
// First lowercase everything
lower := strings.ToLower(s)
runes := []rune(lower)
for i := 0; i < len(runes); i++ {
// Capitalize if previous char is non-letter AND
// (we're at the end OR next char is not space)
if i == 0 || !unicode.IsLetter(runes[i-1]) {
if i == len(runes)-1 || !unicode.IsSpace(runes[i+1]) {
runes[i] = unicode.ToUpper(runes[i])
}
}
}
return string(runes)
}
// Apply executes the requested text operation on the provided value
func (p *TextPlugin) Apply(operation string, value string) (string, error) {
debugf("TextPlugin: operation=%s value=%q", operation, value)
if value == "" {
return "", fmt.Errorf("text: empty input for operation %q", operation)
}
debugf("TextPlugin: operation=%s value=%q", operation, value)
switch operation {
case "upper":
result := strings.ToUpper(value)
debugf("TextPlugin: upper result=%q", result)
return result, nil
case "lower":
result := strings.ToLower(value)
debugf("TextPlugin: lower result=%q", result)
return result, nil
case "title":
result := toTitle(value)
debugf("TextPlugin: title result=%q", result)
return result, nil
if value == "" {
return "", fmt.Errorf("text: empty input for operation %q", operation)
}
case "trim":
result := strings.TrimSpace(value)
debugf("TextPlugin: trim result=%q", result)
return result, nil
default:
return "", fmt.Errorf("text: unknown text operation %q (supported: upper, lower, title, trim)", operation)
}
}
switch operation {
case "upper":
result := strings.ToUpper(value)
debugf("TextPlugin: upper result=%q", result)
return result, nil
case "lower":
result := strings.ToLower(value)
debugf("TextPlugin: lower result=%q", result)
return result, nil
case "title":
result := toTitle(value)
debugf("TextPlugin: title result=%q", result)
return result, nil
case "trim":
result := strings.TrimSpace(value)
debugf("TextPlugin: trim result=%q", result)
return result, nil
default:
return "", fmt.Errorf("text: unknown text operation %q (supported: upper, lower, title, trim)", operation)
}
}

View File

@@ -5,100 +5,100 @@ import (
)
func TestTextPlugin(t *testing.T) {
plugin := &TextPlugin{}
plugin := &TextPlugin{}
tests := []struct {
name string
operation string
value string
want string
wantErr bool
}{
// Upper tests
{
name: "upper basic",
operation: "upper",
value: "hello",
want: "HELLO",
},
{
name: "upper mixed case",
operation: "upper",
value: "hElLo",
want: "HELLO",
},
// Lower tests
{
name: "lower basic",
operation: "lower",
value: "HELLO",
want: "hello",
},
{
name: "lower mixed case",
operation: "lower",
value: "hElLo",
want: "hello",
},
// Title tests
{
name: "title basic",
operation: "title",
value: "hello world",
want: "Hello World",
},
{
name: "title with apostrophe",
operation: "title",
value: "o'reilly's book",
want: "O'Reilly's Book",
},
// Trim tests
{
name: "trim spaces",
operation: "trim",
value: " hello ",
want: "hello",
},
{
name: "trim newlines",
operation: "trim",
value: "\nhello\n",
want: "hello",
},
// Error cases
{
name: "empty value",
operation: "upper",
value: "",
wantErr: true,
},
{
name: "unknown operation",
operation: "invalid",
value: "test",
wantErr: true,
},
}
tests := []struct {
name string
operation string
value string
want string
wantErr bool
}{
// Upper tests
{
name: "upper basic",
operation: "upper",
value: "hello",
want: "HELLO",
},
{
name: "upper mixed case",
operation: "upper",
value: "hElLo",
want: "HELLO",
},
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("TextPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Check successful cases
if err == nil && got != tt.want {
t.Errorf("TextPlugin.Apply() = %q, want %q", got, tt.want)
}
})
}
}
// Lower tests
{
name: "lower basic",
operation: "lower",
value: "HELLO",
want: "hello",
},
{
name: "lower mixed case",
operation: "lower",
value: "hElLo",
want: "hello",
},
// Title tests
{
name: "title basic",
operation: "title",
value: "hello world",
want: "Hello World",
},
{
name: "title with apostrophe",
operation: "title",
value: "o'reilly's book",
want: "O'Reilly's Book",
},
// Trim tests
{
name: "trim spaces",
operation: "trim",
value: " hello ",
want: "hello",
},
{
name: "trim newlines",
operation: "trim",
value: "\nhello\n",
want: "hello",
},
// Error cases
{
name: "empty value",
operation: "upper",
value: "",
wantErr: true,
},
{
name: "unknown operation",
operation: "invalid",
value: "test",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("TextPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Check successful cases
if err == nil && got != tt.want {
t.Errorf("TextPlugin.Apply() = %q, want %q", got, tt.want)
}
})
}
}

211
restapi/chat.go Executable file
View File

@@ -0,0 +1,211 @@
package restapi
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/core"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/gin-gonic/gin"
)
type ChatHandler struct {
registry *core.PluginRegistry
db *fsdb.Db
}
type PromptRequest struct {
UserInput string `json:"userInput"`
Vendor string `json:"vendor"`
Model string `json:"model"`
ContextName string `json:"contextName"`
PatternName string `json:"patternName"`
}
type ChatRequest struct {
Prompts []PromptRequest `json:"prompts"`
common.ChatOptions // Embed the ChatOptions from common package
}
type StreamResponse struct {
Type string `json:"type"` // "content", "error", "complete"
Format string `json:"format"` // "markdown", "mermaid", "plain"
Content string `json:"content"` // The actual content
}
func NewChatHandler(r *gin.Engine, registry *core.PluginRegistry, db *fsdb.Db) *ChatHandler {
handler := &ChatHandler{
registry: registry,
db: db,
}
r.POST("/chat", handler.HandleChat)
return handler
}
func (h *ChatHandler) HandleChat(c *gin.Context) {
var request ChatRequest
if err := c.BindJSON(&request); err != nil {
log.Printf("Error binding JSON: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request format: %v", err)})
return
}
log.Printf("Received chat request with %d prompts", len(request.Prompts))
// Set headers for SSE
c.Writer.Header().Set("Content-Type", "text/readystream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
c.Writer.Header().Set("X-Accel-Buffering", "no")
clientGone := c.Writer.CloseNotify()
for i, prompt := range request.Prompts {
select {
case <-clientGone:
log.Printf("Client disconnected")
return
default:
log.Printf("Processing prompt %d: Model=%s Pattern=%s Context=%s",
i+1, prompt.Model, prompt.PatternName, prompt.ContextName)
// Create chat channel for streaming
streamChan := make(chan string)
// Start chat processing in goroutine
go func(p PromptRequest) {
defer close(streamChan)
chatter, err := h.registry.GetChatter(p.Model, 2048, false, false)
if err != nil {
log.Printf("Error creating chatter: %v", err)
streamChan <- fmt.Sprintf("Error: %v", err)
return
}
chatReq := &common.ChatRequest{
Message: &goopenai.ChatCompletionMessage{
Role: "user",
Content: p.UserInput,
},
PatternName: p.PatternName,
ContextName: p.ContextName,
}
opts := &common.ChatOptions{
Model: p.Model,
Temperature: request.Temperature,
TopP: request.TopP,
FrequencyPenalty: request.FrequencyPenalty,
PresencePenalty: request.PresencePenalty,
}
session, err := chatter.Send(chatReq, opts)
if err != nil {
log.Printf("Error from chatter.Send: %v", err)
streamChan <- fmt.Sprintf("Error: %v", err)
return
}
if session == nil {
log.Printf("No session returned from chatter.Send")
streamChan <- "Error: No response from model"
return
}
// Get the last message from the session
lastMsg := session.GetLastMessage()
if lastMsg != nil {
streamChan <- lastMsg.Content
} else {
log.Printf("No message content in session")
streamChan <- "Error: No response content"
}
}(prompt)
// Read from streamChan and write to client
for content := range streamChan {
select {
case <-clientGone:
return
default:
if strings.HasPrefix(content, "Error:") {
response := StreamResponse{
Type: "error",
Format: "plain",
Content: content,
}
if err := writeSSEResponse(c.Writer, response); err != nil {
log.Printf("Error writing error response: %v", err)
return
}
} else {
response := StreamResponse{
Type: "content",
Format: detectFormat(content),
Content: content,
}
if err := writeSSEResponse(c.Writer, response); err != nil {
log.Printf("Error writing content response: %v", err)
return
}
}
}
}
// Signal completion of this prompt
completeResponse := StreamResponse{
Type: "complete",
Format: "plain",
Content: "",
}
if err := writeSSEResponse(c.Writer, completeResponse); err != nil {
log.Printf("Error writing completion response: %v", err)
return
}
}
}
}
func writeSSEResponse(w gin.ResponseWriter, response StreamResponse) error {
data, err := json.Marshal(response)
if err != nil {
return fmt.Errorf("error marshaling response: %v", err)
}
if _, err := fmt.Fprintf(w, "data: %s\n\n", string(data)); err != nil {
return fmt.Errorf("error writing response: %v", err)
}
w.(http.Flusher).Flush()
return nil
}
func detectFormat(content string) string {
if strings.HasPrefix(content, "graph TD") ||
strings.HasPrefix(content, "gantt") ||
strings.HasPrefix(content, "flowchart") ||
strings.HasPrefix(content, "sequenceDiagram") ||
strings.HasPrefix(content, "classDiagram") ||
strings.HasPrefix(content, "stateDiagram") {
return "mermaid"
}
if strings.Contains(content, "```") ||
strings.Contains(content, "#") ||
strings.Contains(content, "*") ||
strings.Contains(content, "_") ||
strings.Contains(content, "-") {
return "markdown"
}
return "plain"
}

124
restapi/configuration.go Executable file
View File

@@ -0,0 +1,124 @@
package restapi
import (
"fmt"
"net/http"
"os"
"strings"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/gin-gonic/gin"
)
// ConfigHandler defines the handler for configuration-related operations
type ConfigHandler struct {
db *fsdb.Db
// configurations *fsdb.EnvFilePath("$HOME/.config/fabric/.env")
}
func NewConfigHandler(r *gin.Engine, db *fsdb.Db) *ConfigHandler {
handler := &ConfigHandler{
db: db,
// configurations: db.Configurations,
}
r.GET("/config", handler.GetConfig)
r.POST("/config/update", handler.UpdateConfig)
return handler
}
func (h *ConfigHandler) GetConfig(c *gin.Context) {
if h.db == nil {
c.JSON(http.StatusNotFound, gin.H{"error": ".env file not found"})
return
}
if !h.db.IsEnvFileExists() {
c.JSON(http.StatusOK, gin.H{
"openai": "",
"anthropic": "",
"groq": "",
"mistral": "",
"gemini": "",
"ollama": "",
"openrouter": "",
"silicon": "",
})
return
}
err := h.db.LoadEnvFile()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
config := map[string]string{
"openai": os.Getenv("OPENAI_API_KEY"),
"anthropic": os.Getenv("ANTHROPIC_API_KEY"),
"groq": os.Getenv("GROQ_API_KEY"),
"mistral": os.Getenv("MISTRAL_API_KEY"),
"gemini": os.Getenv("GEMINI_API_KEY"),
"ollama": os.Getenv("OLLAMA_URL"),
"openrouter": os.Getenv("OPENROUTER_API_KEY"),
"silicon": os.Getenv("SILICON_API_KEY"),
}
c.JSON(http.StatusOK, config)
}
func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
if h.db == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database not initialized"})
return
}
var config struct {
OpenAIApiKey string `json:"openai_api_key"`
AnthropicApiKey string `json:"anthropic_api_key"`
GroqApiKey string `json:"groq_api_key"`
MistralApiKey string `json:"mistral_api_key"`
GeminiApiKey string `json:"gemini_api_key"`
OllamaURL string `json:"ollama_url"`
OpenRouterApiKey string `json:"openrouter_api_key"`
SiliconApiKey string `json:"silicon_api_key"`
}
if err := c.BindJSON(&config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
envVars := map[string]string{
"OPENAI_API_KEY": config.OpenAIApiKey,
"ANTHROPIC_API_KEY": config.AnthropicApiKey,
"GROQ_API_KEY": config.GroqApiKey,
"MISTRAL_API_KEY": config.MistralApiKey,
"GEMINI_API_KEY": config.GeminiApiKey,
"OLLAMA_URL": config.OllamaURL,
"OPENROUTER_API_KEY": config.OpenRouterApiKey,
"SILICON_API_KEY": config.SiliconApiKey,
}
var envContent strings.Builder
for key, value := range envVars {
if value != "" {
envContent.WriteString(fmt.Sprintf("%s=%s\n", key, value))
os.Setenv(key, value)
}
}
// Save configuration to file
if err := h.db.SaveEnv(envContent.String()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := h.db.LoadEnvFile(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Configuration updated successfully"})
}

45
restapi/models.go Executable file
View File

@@ -0,0 +1,45 @@
package restapi
import (
"github.com/danielmiessler/fabric/plugins/ai"
"github.com/gin-gonic/gin"
)
type ModelsHandler struct {
vendorManager *ai.VendorsManager
}
func NewModelsHandler(r *gin.Engine, vendorManager *ai.VendorsManager) {
handler := &ModelsHandler{
vendorManager: vendorManager,
}
r.GET("/models/names", handler.GetModelNames)
}
func (h *ModelsHandler) GetModelNames(c *gin.Context) {
vendorsModels, err := h.vendorManager.GetModels()
if err != nil {
c.JSON(500, gin.H{"error": "Server failed to retrieve model names"})
return
}
response := make(map[string]interface{})
vendors := make(map[string][]string)
for _, groupItems := range vendorsModels.GroupsItems {
vendors[groupItems.Group] = groupItems.Items
}
response["models"] = h.getAllModelNames(vendorsModels)
response["vendors"] = vendors
c.JSON(200, response)
}
func (h *ModelsHandler) getAllModelNames(vendorsModels *ai.VendorsModels) []string {
var allModelNames []string
for _, groupItems := range vendorsModels.GroupsItems {
allModelNames = append(allModelNames, groupItems.Items...)
}
return allModelNames
}

View File

@@ -27,7 +27,7 @@ func NewPatternsHandler(r *gin.Engine, patterns *fsdb.PatternsEntity) (ret *Patt
func (h *PatternsHandler) Get(c *gin.Context) {
name := c.Param("name")
variables := make(map[string]string) // Assuming variables are passed somehow
input := "" // Assuming input is passed somehow
input := "" // Assuming input is passed somehow
pattern, err := h.patterns.GetApplyVariables(name, variables, input)
if err != nil {
c.JSON(http.StatusInternalServerError, err.Error())

View File

@@ -17,6 +17,9 @@ func Serve(registry *core.PluginRegistry, address string) (err error) {
NewPatternsHandler(r, fabricDb.Patterns)
NewContextsHandler(r, fabricDb.Contexts)
NewSessionsHandler(r, fabricDb.Sessions)
NewChatHandler(r, registry, fabricDb)
NewConfigHandler(r, fabricDb)
NewModelsHandler(r, registry.VendorManager)
// Start server
err = r.Run(address)

View File

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

11
web/.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm", "pnpm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

0
web/.npmrc Normal file
View File

4
web/.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

8
web/.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

120
web/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,120 @@
{
"prettier.documentSelectors": [
"**/*.svelte"
],
"tailwindCSS.classAttributes": [
"class",
"accent",
"active",
"animIndeterminate",
"aspectRatio",
"background",
"badge",
"bgBackdrop",
"bgDark",
"bgDrawer",
"bgLight",
"blur",
"border",
"button",
"buttonAction",
"buttonBack",
"buttonClasses",
"buttonComplete",
"buttonDismiss",
"buttonNeutral",
"buttonNext",
"buttonPositive",
"buttonTextCancel",
"buttonTextConfirm",
"buttonTextFirst",
"buttonTextLast",
"buttonTextNext",
"buttonTextPrevious",
"buttonTextSubmit",
"caretClosed",
"caretOpen",
"chips",
"color",
"controlSeparator",
"controlVariant",
"cursor",
"display",
"element",
"fill",
"fillDark",
"fillLight",
"flex",
"flexDirection",
"gap",
"gridColumns",
"height",
"hover",
"inactive",
"indent",
"justify",
"meter",
"padding",
"position",
"regionAnchor",
"regionBackdrop",
"regionBody",
"regionCaption",
"regionCaret",
"regionCell",
"regionChildren",
"regionChipList",
"regionChipWrapper",
"regionCone",
"regionContent",
"regionControl",
"regionDefault",
"regionDrawer",
"regionFoot",
"regionFootCell",
"regionFooter",
"regionHead",
"regionHeadCell",
"regionHeader",
"regionIcon",
"regionInput",
"regionInterface",
"regionInterfaceText",
"regionLabel",
"regionLead",
"regionLegend",
"regionList",
"regionListItem",
"regionNavigation",
"regionPage",
"regionPanel",
"regionRowHeadline",
"regionRowMain",
"regionSummary",
"regionSymbol",
"regionTab",
"regionTrail",
"ring",
"rounded",
"select",
"shadow",
"slotDefault",
"slotFooter",
"slotHeader",
"slotLead",
"slotMessage",
"slotMeta",
"slotPageContent",
"slotPageFooter",
"slotPageHeader",
"slotSidebarLeft",
"slotSidebarRight",
"slotTrail",
"spacing",
"text",
"track",
"transition",
"width",
"zIndex"
]
}

13
web/README.md Normal file
View File

@@ -0,0 +1,13 @@
## The Fabric Web App
[Installing](#Installing)|[Todos](#Todos)|[Collaborators](#Collaborators)
This is the web app for Fabric. It is built using [Svelte](https://svelte.dev/), [SkeletonUI](https://skeleton.dev/), and [Mdsvex](https://mdsvex.pngwn.io/).
The goal of this app is to not only provide a user interface for Fabric, but also a out-of-the-box website for those who want to get started with web development or blogging. The tech stack is minimal and (I hope) the code is easy to read and understand. One thing I kept in mind when making this app was to make it easy for beginners to get started with web development. You can use this app as a GUI interface for Fabric, a ready to go blog-site, or a website template for your own projects. I hope you find it useful!
![Preview](static/image.png)
### Installing
It can be installed by navigating to the `web` directory and using `npm install`, `pnpm install`, or your favorite package manager. Then simply run `npm run dev` or your equivalent command to start the app. *You will need to run fabric in a seperate terminal with the `fabric --serve` command.*

21
web/SECURITY.md Normal file
View File

@@ -0,0 +1,21 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.

38
web/STD-README.md Normal file
View File

@@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

32
web/eslint.config.js Normal file
View File

@@ -0,0 +1,32 @@
import eslint from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: tseslint.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
);

19
web/jsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

4
web/markdown.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
/* declare module '*.md' {
const component: import('svelte').ComponentType;
export default component;
} */

102
web/my-custom-theme.ts Normal file
View File

@@ -0,0 +1,102 @@
import type { CustomThemeConfig } from '@skeletonlabs/tw-plugin';
export const myCustomTheme: CustomThemeConfig = {
name: 'my-custom-theme',
properties: {
// =~= Theme Properties =~=
"--theme-font-family-base": `system-ui`,
"--theme-font-family-heading": `system-ui`,
"--theme-font-color-base": "var(--color-primary-800)",
"--theme-font-color-dark": "var(--color-primary-300)",
"--theme-rounded-base": "9999px",
"--theme-rounded-container": "8px",
"--theme-border-base": "1px",
// =~= Theme On-X Colors =~=
"--on-primary": "0 0 0",
"--on-secondary": "0 0 0",
"--on-tertiary": "0 0 0",
"--on-success": "0 0 0",
"--on-warning": "0 0 0",
"--on-error": "0 0 0",
"--on-surface": "0 0 0",
// =~= Theme Colors =~=
// primary | #613bf7
"--color-primary-50": "231 226 254", // #e7e2fe
"--color-primary-100": "223 216 253", // #dfd8fd
"--color-primary-200": "216 206 253", // #d8cefd
"--color-primary-300": "192 177 252", // #c0b1fc
"--color-primary-400": "144 118 249", // #9076f9
"--color-primary-500": "97 59 247", // #613bf7
"--color-primary-600": "87 53 222", // #5735de
"--color-primary-700": "73 44 185", // #492cb9
"--color-primary-800": "58 35 148", // #3a2394
"--color-primary-900": "48 29 121", // #301d79
// secondary | #9de1ae
"--color-secondary-50": "240 251 243", // #f0fbf3
"--color-secondary-100": "235 249 239", // #ebf9ef
"--color-secondary-200": "231 248 235", // #e7f8eb
"--color-secondary-300": "216 243 223", // #d8f3df
"--color-secondary-400": "186 234 198", // #baeac6
"--color-secondary-500": "157 225 174", // #9de1ae
"--color-secondary-600": "141 203 157", // #8dcb9d
"--color-secondary-700": "118 169 131", // #76a983
"--color-secondary-800": "94 135 104", // #5e8768
"--color-secondary-900": "77 110 85", // #4d6e55
// tertiary | #3fa0a6
"--color-tertiary-50": "226 241 242", // #e2f1f2
"--color-tertiary-100": "217 236 237", // #d9eced
"--color-tertiary-200": "207 231 233", // #cfe7e9
"--color-tertiary-300": "178 217 219", // #b2d9db
"--color-tertiary-400": "121 189 193", // #79bdc1
"--color-tertiary-500": "63 160 166", // #3fa0a6
"--color-tertiary-600": "57 144 149", // #399095
"--color-tertiary-700": "47 120 125", // #2f787d
"--color-tertiary-800": "38 96 100", // #266064
"--color-tertiary-900": "31 78 81", // #1f4e51
// success | #37b3fc
"--color-success-50": "225 244 255", // #e1f4ff
"--color-success-100": "215 240 254", // #d7f0fe
"--color-success-200": "205 236 254", // #cdecfe
"--color-success-300": "175 225 254", // #afe1fe
"--color-success-400": "115 202 253", // #73cafd
"--color-success-500": "55 179 252", // #37b3fc
"--color-success-600": "50 161 227", // #32a1e3
"--color-success-700": "41 134 189", // #2986bd
"--color-success-800": "33 107 151", // #216b97
"--color-success-900": "27 88 123", // #1b587b
// warning | #d209f8
"--color-warning-50": "248 218 254", // #f8dafe
"--color-warning-100": "246 206 254", // #f6cefe
"--color-warning-200": "244 194 253", // #f4c2fd
"--color-warning-300": "237 157 252", // #ed9dfc
"--color-warning-400": "224 83 250", // #e053fa
"--color-warning-500": "210 9 248", // #d209f8
"--color-warning-600": "189 8 223", // #bd08df
"--color-warning-700": "158 7 186", // #9e07ba
"--color-warning-800": "126 5 149", // #7e0595
"--color-warning-900": "103 4 122", // #67047a
// error | #90df16
"--color-error-50": "238 250 220", // #eefadc
"--color-error-100": "233 249 208", // #e9f9d0
"--color-error-200": "227 247 197", // #e3f7c5
"--color-error-300": "211 242 162", // #d3f2a2
"--color-error-400": "177 233 92", // #b1e95c
"--color-error-500": "144 223 22", // #90df16
"--color-error-600": "130 201 20", // #82c914
"--color-error-700": "108 167 17", // #6ca711
"--color-error-800": "86 134 13", // #56860d
"--color-error-900": "71 109 11", // #476d0b
// surface | #46a1ed
"--color-surface-50": "227 241 252", // #e3f1fc
"--color-surface-100": "218 236 251", // #daecfb
"--color-surface-200": "209 232 251", // #d1e8fb
"--color-surface-300": "181 217 248", // #b5d9f8
"--color-surface-400": "126 189 242", // #7ebdf2
"--color-surface-500": "70 161 237", // #46a1ed
"--color-surface-600": "63 145 213", // #3f91d5
"--color-surface-700": "53 121 178", // #3579b2
"--color-surface-800": "42 97 142", // #2a618e
"--color-surface-900": "34 79 116", // #224f74
}
}

7397
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
web/package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "terminal-blog",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@skeletonlabs/skeleton": "^2.8.0",
"@skeletonlabs/tw-plugin": "^0.3.1",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.8.4",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.10.0",
"autoprefixer": "^10.4.16",
"lucide-svelte": "^0.309.0",
"mdsvex": "^0.11.0",
"postcss": "^8.4.32",
"postcss-load-config": "^5.0.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"svelte-youtube-embed": "^0.3.3",
"tailwindcss": "^3.3.6",
"typescript": "^5.0.0",
"vite": "^5.0.3",
"vite-plugin-tailwind-purgecss": "^0.2.0"
},
"type": "module",
"dependencies": {
"@floating-ui/dom": "^1.5.3",
"clsx": "^2.1.1",
"cn": "^0.1.1",
"date-fns": "^4.1.0",
"highlight.js": "^11.10.0",
"marked": "^15.0.1",
"rehype": "^13.0.2",
"rehype-autolink-headings": "^7.1.0",
"rehype-external-links": "^3.0.0",
"rehype-slug": "^6.0.0",
"rehype-unwrap-images": "^1.0.0",
"tailwind-merge": "^2.5.4",
"youtube-transcript": "^1.2.1"
}
}

2925
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
web/postcss.config.cjs Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

6
web/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

22
web/rollup.config.js Normal file
View File

@@ -0,0 +1,22 @@
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
export default {
input: 'src/+page.js',
output: {
file: 'public/bundle.js',
format: 'iife',
name: 'app'
},
plugins: [
svelte({
// svelte options
extensions: [".svelte", ".svx", ".md"],
preprocess: mdsvex()
}),
resolve({
browser: true,
dedupe: ['svelte']
})
]
};

134
web/src/app.css Normal file
View File

@@ -0,0 +1,134 @@
/* @tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Go Mono', 'Fira Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Code', monospace;
--column-width: 42rem;
--column-margin-top: 4rem;
} */
/* Light theme variables */
/* :root {
--background: hsl(249, 81%, 85%);
--foreground: hsl(229, 65%, 29%);
--card: 0 0% 100%;
--card-foreground: 224 71.4% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--primary: hsl(262.1 83.3% 57.8%);
--primary-foreground: hsl(274, 100%, 90%);
--secondary: hsl(173, 74%, 68%);
--secondary-foreground: hsl(195, 100%, 90%);
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--accent: hsl(220, 37%, 49%);
--accent-foreground: 220.9 39.3% 11%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 20% 98%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 262.1 83.3% 57.8%;
--radius: 0.5rem;
} */
/* Dark theme variables */
/* .dark {
--background: 224 71.4% 4.1%;
--foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card-foreground: 210 20% 98%;
--popover: 224 71.4% 4.1%;
--popover-foreground: 210 20% 98%;
--primary: 263.4 70% 50.4%;
--primary-foreground: 210 20% 98%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--ring: 263.4 70% 50.4%;
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
} */
/* Enhanced Typography */
/* h1, h2, h3, h4, h5, h6 {
@apply font-semibold tracking-tight;
}
h1 {
@apply text-4xl lg:text-5xl;
}
h2 {
@apply text-3xl lg:text-4xl;
}
h3 {
@apply text-2xl lg:text-3xl;
}
p {
@apply leading-7;
}
*/
/* Links */
/* a {
@apply text-primary hover:text-primary/80 transition-colors;
} */
/* Code blocks */
/* pre {
@apply p-4 rounded-lg bg-muted/50 font-mono text-sm;
}
code {
@apply font-mono text-sm;
}
*/
/* Terminal specific styles */
/* .terminal-window {
@apply rounded-lg border bg-card shadow-lg overflow-hidden;
}
.terminal-text {
@apply font-mono text-sm;
}
*/
/* Form elements */
/* input, textarea, select {
@apply rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
}
button {
@apply inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
}
} */
/* Custom scrollbar */
/* ::-webkit-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-track {
@apply bg-muted;
}
::-webkit-scrollbar-thumb {
@apply bg-muted-foreground/30 rounded-full hover:bg-muted-foreground/50 transition-colors;
} */

9
web/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
interface PageData {}
// interface Error {}
// interface Platform {}
}

12
web/src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" data-theme="my-custom-theme">
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
</body>
</html>

25
web/src/app.postcss Normal file
View File

@@ -0,0 +1,25 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
html,
body {
@apply h-full overflow-hidden;
}
.terminal-output {
@apply font-mono text-sm;
}
.terminal-input {
@apply font-mono text-sm;
}
/* Skeleton theme overrides */
:root [data-theme='skeleton'] {
--theme-font-family-base: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--theme-font-family-heading: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

7
web/src/index.test.ts Normal file
View File

@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { cn } from "$lib/types/utils";
import { buttonVariants } from "./index.js";
let className = undefined;
export let variant = "";
export let size = "default";
export { className as class };
$: classes = cn(
buttonVariants.base,
buttonVariants.variants.variant[variant as keyof typeof buttonVariants.variants.variant],
buttonVariants.variants.size[size as keyof typeof buttonVariants.variants.size],
className
);
</script>
<button
class={classes}
type="button"
on:click
on:keydown
{...$$restProps}
>
<slot />
</button>

View File

@@ -0,0 +1,31 @@
import Root from "./button.svelte";
const buttonVariants = {
base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
outline: "border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
};
export {
Root,
Root as Button,
buttonVariants
};

View File

@@ -0,0 +1,32 @@
<script lang="ts">
export let url: string = 'https://www.buymeacoffee.com/johnconnor.sec';
export let text: string = 'Buy me a coffee';
</script>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class="text-sm px-3 py-1.5 btn variant-filled-tertiary hover:variant-filled-secondary transition-all duration-200 flex items-center gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="transition-transform duration-200 group-hover:rotate-12"
>
<path d="M17 8h1a4 4 0 1 1 0 8h-1" />
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" />
<line x1="6" y1="2" x2="6" y2="4" />
<line x1="10" y1="2" x2="10" y2="4" />
<line x1="14" y1="2" x2="14" y2="4" />
</svg>
{text}
</a>

View File

@@ -0,0 +1,6 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import { cn } from "$lib/types/utils";
let className: string | undefined = undefined;
export let value = undefined;
export { className as class };
export let readonly = undefined;
</script>
<input
class={cn(
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-lg transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>

View File

@@ -0,0 +1,6 @@
import Root from "./label.svelte";
export {
Root,
Root as Label
};

View File

@@ -0,0 +1,15 @@
<script>
import { cn } from "$lib/types/utils.ts";
let className = undefined;
export { className as class };
</script>
<label
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
>
<slot />
</label>

View File

@@ -0,0 +1,6 @@
import Root from "./select.svelte";
export {
Root,
Root as Select
};

View File

@@ -0,0 +1,30 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { getContext } from "svelte";
let className = undefined;
export let sideOffset = 4;
export let position = "popper";
</script>
<div
class={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
>
<SelectPrimitive.Content {position} {sideOffset} {...$$restProps}>
<div
class={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
<slot />
</div>
</SelectPrimitive.Content>
</div>

View File

@@ -0,0 +1,16 @@
<script>
import { cn } from "$lib/utils";
let className = undefined;
export let value;
export let disabled = false;
export { className as class };
</script>
<option
{value}
{disabled}
class={cn("relative flex w-full cursor-default select-none py-1.5 pl-2 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className)}
>
<slot />
</option>

View File

@@ -0,0 +1,10 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/types/utils.ts";
let className = undefined;
export { className as class };
</script>
<SelectPrimitive.Label class={cn("px-2 py-1.5 text-sm variant-filled-secondary font-semibold", className)} {...$$restProps}>
<slot />
</SelectPrimitive.Label>

View File

@@ -0,0 +1,8 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/types/utils.ts";
let className = undefined;
export { className as class };
</script>
<SelectPrimitive.Separator class={cn("bg-muted -mx-1 my-1 h-px", className)} {...$$restProps} />

View File

@@ -0,0 +1,20 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { ChevronDown } from "lucide-svelte";
import { cn } from "$lib/utils";
import { getContext } from "svelte";
let className = undefined;
export { className as class };
</script>
<SelectPrimitive.Trigger
class={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...$$restProps}
>
<slot />
<ChevronDown class="h-4 w-4 opacity-50" />
</SelectPrimitive.Trigger>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type $$Props = SelectPrimitive.ValueProps;
let className: $$Props["class"] = undefined;
export { className as class };
export let placeholder: $$Props["placeholder"] = undefined;
</script>
<SelectPrimitive.Value
class={cn("text-sm", className)}
{placeholder}
{...$$restProps}
>
<slot />
</SelectPrimitive.Value>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from "$lib/types/utils.ts";
import { ChevronDown } from "lucide-svelte";
export let value: any = undefined;
export let disabled = false;
let className = undefined;
export { className as class };
</script>
<div class="relative">
<select
{disabled}
bind:value
class={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-lg ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 appearance-none",
className
)}
{...$$restProps}
>
<slot />
</select>
<ChevronDown class="absolute right-3 top-2.5 h-4 w-4 opacity-50 pointer-events-none" />
</div>

View File

@@ -0,0 +1,6 @@
import Root from "./slider.svelte";
export {
Root,
//
Root as Slider,
};

View File

@@ -0,0 +1,89 @@
<script>
import { cn } from "$lib/types/utils.ts";
let className = undefined;
export let value = 0;
export let min = 0;
export let max = 100;
export let step = 1;
export { className as class };
let sliderEl;
let isDragging = false;
$: percentage = ((value - min) / (max - min)) * 100;
function handleMouseDown(e) {
isDragging = true;
updateValue(e);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
}
function handleMouseMove(e) {
if (!isDragging) return;
updateValue(e);
}
function handleMouseUp() {
isDragging = false;
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
function updateValue(e) {
if (!sliderEl) return;
const rect = sliderEl.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const rawValue = min + (max - min) * pos;
const steppedValue = Math.round(rawValue / step) * step;
value = Math.max(min, Math.min(max, steppedValue));
}
function handleKeyDown(e) {
const step = e.shiftKey ? 10 : 1;
switch (e.key) {
case 'ArrowLeft':
case 'ArrowDown':
e.preventDefault();
value = Math.max(min, value - step);
break;
case 'ArrowRight':
case 'ArrowUp':
e.preventDefault();
value = Math.min(max, value + step);
break;
case 'Home':
e.preventDefault();
value = min;
break;
case 'End':
e.preventDefault();
value = max;
break;
}
}
</script>
<div
bind:this={sliderEl}
class={cn("relative flex w-full touch-none select-none items-center", className)}
on:mousedown={handleMouseDown}
on:keydown={handleKeyDown}
role="slider"
tabindex="0"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
>
<div class="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<div
class="absolute h-full bg-primary transition-all"
style="width: {percentage}%"
/>
</div>
<div
class="absolute h-4 w-4 rounded-full border border-secondary bg-primary-500 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
style="left: calc({percentage}% - 0.5rem)"
/>
</div>

View File

@@ -0,0 +1,9 @@
<script lang="ts">
import { cn } from '$lib/types/utils';
import { Loader2 } from 'lucide-svelte';
let className: string | undefined = undefined;
export { className as class };
</script>
<Loader2 class={cn('h-4 w-4 animate-spin', className)} {...$$restProps} />

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import { ChevronLeft, ChevronRight } from 'lucide-svelte';
import { slide } from 'svelte/transition';
import { cn } from '$lib/types/utils';
export let tags: string[] = [];
export let tagsPerPage = 5;
export let className: string | undefined = undefined;
let currentPage = 0;
let containerWidth: number;
$: totalPages = Math.ceil(tags.length / tagsPerPage);
$: startIndex = currentPage * tagsPerPage;
$: endIndex = Math.min(startIndex + tagsPerPage, tags.length);
$: visibleTags = tags.slice(startIndex, endIndex);
$: canGoBack = currentPage > 0;
$: canGoForward = currentPage < totalPages - 1;
function nextPage() {
if (canGoForward) {
currentPage++;
}
}
function prevPage() {
if (canGoBack) {
currentPage--;
}
}
</script>
<div class={cn('relative flex items-center gap-2', className)} bind:clientWidth={containerWidth}>
{#if totalPages > 1 && canGoBack}
<button
on:click={prevPage}
class="flex h-6 w-6 items-center justify-center rounded-md border bg-background hover:bg-muted"
transition:slide
>
<ChevronLeft class="h-4 w-4" />
<span class="sr-only">Previous page</span>
</button>
{/if}
<div class="flex flex-wrap gap-2">
{#each visibleTags as tag (tag)}
<a
href="/tags/{tag}"
class="inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-muted"
>
{tag}
</a>
{/each}
</div>
{#if totalPages > 1 && canGoForward}
<button
on:click={nextPage}
class="flex h-6 w-6 items-center justify-center rounded-md border bg-background hover:bg-muted"
transition:slide
>
<ChevronRight class="h-4 w-4" />
<span class="sr-only">Next page</span>
</button>
{/if}
</div>

View File

@@ -0,0 +1,6 @@
import Root from "./textarea.svelte";
export {
Root,
//
Root as Textarea,
};

View File

@@ -0,0 +1,29 @@
<script>
import { cn } from "$lib/types/utils.ts";
let className = undefined;
export let value = undefined;
export { className as class };
export let readonly = undefined;
</script>
<textarea
class={cn(
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-lg focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
></textarea>

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,30 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"properties": false,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": false
}

View File

@@ -0,0 +1,5 @@
{
"folder": "templates",
"dateFormat": "YYYY-MM-DD",
"timeFormat": "HH:mm"
}

View File

@@ -0,0 +1,8 @@
{
"types": {
"aliases": "aliases",
"cssclasses": "multitext",
"tags": "tags",
"updated": "datetime"
}
}

View File

@@ -0,0 +1,173 @@
{
"main": {
"id": "d2e57b203fabd791",
"type": "split",
"children": [
{
"id": "bede7cd0fb75a7df",
"type": "tabs",
"children": [
{
"id": "c588c8d12c5f7702",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "templates/{{title}}.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "{{title}}"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "a69ef8c1dea71399",
"type": "split",
"children": [
{
"id": "99030135b6260693",
"type": "tabs",
"children": [
{
"id": "bbeb4ea8d01ce855",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical"
},
"icon": "lucide-folder-closed",
"title": "Files"
}
},
{
"id": "afc5509c38fa5543",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "Search"
}
},
{
"id": "53c950f1571616a8",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "Bookmarks"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "74142d853a5fb911",
"type": "split",
"children": [
{
"id": "065cd0d2b52977db",
"type": "tabs",
"children": [
{
"id": "398f4b2bc7fb48c1",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"file": "posts/welcome.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "Backlinks for welcome"
}
},
{
"id": "655e694ad24637c7",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"file": "posts/welcome.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "Outgoing links from welcome"
}
},
{
"id": "eba769dfb90abcb3",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true
},
"icon": "lucide-tags",
"title": "Tags"
}
},
{
"id": "2bcc1385d707df56",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"file": "posts/welcome.md"
},
"icon": "lucide-list",
"title": "Outline of welcome"
}
}
]
}
],
"direction": "horizontal",
"width": 300,
"collapsed": true
},
"left-ribbon": {
"hiddenItems": {
"switcher:Open quick switcher": false,
"graph:Open graph view": false,
"canvas:Create new canvas": false,
"daily-notes:Open today's daily note": false,
"templates:Insert template": false,
"command-palette:Open command palette": false
}
},
"active": "c588c8d12c5f7702",
"lastOpenFiles": [
"posts/welcome.md",
"templates/{{title}}.md",
"posts/SkeletonUI.md",
"posts/getting-started.md",
"posts/extract_wisdom.md"
]
}

View File

@@ -0,0 +1,226 @@
---
title: SkeletonUI
tags:
- svelte
- styling
- skeletonui
date: 2023-01-17
---
SkeletonUI is a comprehensive UI toolkit that integrates seamlessly with SvelteKit and Tailwind CSS, enabling developers to build adaptive and accessible web interfaces efficiently.
SkeletonUI offers a comprehensive suite of components to enhance your Svelte applications. Below is a categorized list of these components, presented in Svelte syntax:
```svelte
<!-- Layout Components -->
<AppShell />
<AppBar />
<Sidebar />
<Footer />
<!-- Navigation Components -->
<NavMenu />
<Breadcrumbs />
<Tabs />
<Pagination />
<!-- Form Components -->
<Button />
<Input />
<Select />
<Textarea />
<Checkbox />
<Radio />
<Switch />
<Slider />
<FileUpload />
<!-- Data Display Components -->
<Card />
<Avatar />
<Badge />
<Chip />
<Divider />
<Table />
<List />
<Accordion />
<ProgressBar />
<Rating />
<Tag />
<!-- Feedback Components -->
<Alert />
<Modal />
<Toast />
<Popover />
<Tooltip />
<!-- Media Components -->
<Image />
<Video />
<Icon />
<!-- Utility Components -->
<Spinner />
<SkeletonLoader />
<Placeholder />
```
For detailed information on each component, including their properties and usage examples, please refer to the official SkeletonUI documentation.
___
Below is an expanded cheat sheet to assist you in leveraging SkeletonUI within your SvelteKit projects.
**1\. Installation**
To set up SkeletonUI in a new SvelteKit project, follow these steps:
- **Create a new SvelteKit project**:
```bash
npx sv create my-skeleton-app
cd my-skeleton-app
npm install
```
- **Install SkeletonUI packages**:
```bash
npm install -D @skeletonlabs/skeleton@next @skeletonlabs/skeleton-svelte@next
```
- **Configure Tailwind CSS**:
In your `tailwind.config.js` file, add the following:
```javascript
import { skeleton, contentPath } from '@skeletonlabs/skeleton/plugin';
import * as themes from '@skeletonlabs/skeleton/themes';
export default {
content: [
'./src/**/*.{html,js,svelte,ts}',
contentPath(import.meta.url, 'svelte')
],
theme: {
extend: {},
},
plugins: [
skeleton({
themes: [themes.cerberus, themes.rose]
})
]
};
```
- **Set the active theme**:
In your `src/app.html`, set the `data-theme` attribute on the `<body>` tag:
```html
<body data-theme="cerberus">
<!-- Your content -->
</body>
```
For detailed installation instructions, refer to the official SkeletonUI documentation.
**2\. Components**
SkeletonUI offers a variety of pre-designed components to accelerate your development process. Here's how to use some of them:
- **Button**:
```svelte
<script>
import { Button } from '@skeletonlabs/skeleton-svelte';
</script>
<Button on:click={handleClick}>Click Me</Button>
```
- **Card**:
```svelte
<script>
import { Card } from '@skeletonlabs/skeleton-svelte';
</script>
<Card>
<h2>Card Title</h2>
<p>Card content goes here.</p>
</Card>
```
- **Form Input**:
```svelte
<script>
import { Input } from '@skeletonlabs/skeleton-svelte';
let inputValue = '';
</script>
<Input bind:value={inputValue} placeholder="Enter text" />
```
For a comprehensive list of components and their usage, consult the SkeletonUI components documentation.
**3\. Theming**
SkeletonUI's theming system allows for extensive customization:
- **Applying a Theme**:
Set the desired theme in your `tailwind.config.js` and `app.html` as shown in the installation steps above.
- **Switching Themes Dynamically**:
To enable dynamic theme switching, you can modify the `data-theme` attribute programmatically:
```svelte
<script>
function switchTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
</script>
<button on:click={() => switchTheme('rose')}>Switch to Rose Theme</button>
```
- **Customizing Themes**:
You can create custom themes by defining your own color palettes and settings in the `tailwind.config.js` file.
For more information on theming, refer to the SkeletonUI theming guide.
**4\. Utilities**
SkeletonUI provides several utility functions and actions to enhance your SvelteKit application:
- **Table of Contents**:
Automatically generate a table of contents based on page headings:
```svelte
<script>
import { TableOfContents, tocCrawler } from '@skeletonlabs/skeleton-svelte';
</script>
<div use:tocCrawler>
<TableOfContents />
<!-- Your content with headings -->
</div>
```
- **Transitions and Animations**:
Utilize built-in transitions for smooth animations:
```svelte
<script>
import { fade } from '@skeletonlabs/skeleton-svelte';
let visible = true;
</script>
{#if visible}
<div transition:fade>
Fading content
</div>
{/if}
```
For a full list of utilities and their usage, explore the SkeletonUI utilities documentation.
This cheat sheet provides a foundational overview to help you start integrating SkeletonUI into your SvelteKit projects. For more detailed information and advanced features, please refer to the official SkeletonUI documentation.
https://www.skeleton.dev/docs/introduction

View File

@@ -0,0 +1,135 @@
---
title: Using Markdown in Svelte
description: Learn how to use your markdown documents in Svelte Applications!
date: 2023-12-22
tags: [markdown, svelte,web-dev, documentation]
---
[Mdsvex](https://mdsvex.pngwn.io/docs#install-it)
Here are some examples illustrating how to use Mdsvex in a Svelte application:
**Example 1**: Basic Markdown with Svelte Component
Create a file named example.svx:
markdown
```markdown
---
title: "Interactive Markdown Example"
---
<script>
import Counter from '../components/Counter.svelte';
</script>
# {title}
This is an example of combining Markdown with a Svelte component:
<Counter />
```
In this example:
- The frontmatter (--- sections) defines variables like title.
- A Svelte component Counter is imported and used inside the Markdown.
**Example 2**: Custom Layouts with Mdsvex
Assuming you have a layout component at src/lib/layouts/BlogLayout.svelte:
svelte
```text
<!-- BlogLayout.svelte -->
<script>
export let title;
</script>
<div class="blog-post">
<h1>{title}</h1>
<slot />
</div>
```
Now, to use this layout in your Markdown:
**markdown**
```markdown
---
title: "My Favorite Layout"
layout: "../lib/layouts/BlogLayout.svelte"
---
## Markdown with Custom Layout
This Markdown file will be wrapped by the `BlogLayout`.
```
**Example 3:** Using Frontmatter Variables in Markdown
**markdown**
```markdown
---
author: "John Doe"
date: "2024-11-15"
---
# Blog Post
By {author} on {date}
Here's some markdown content. You can reference frontmatter values directly in the body.
```
**Example 4**: Interactive Elements in Markdown
markdown
```markdown
---
title: "Interactive Chart"
---
<script>
import { Chart } from '../components/Chart.svelte';
</script>
# {title}
Below is an interactive chart:
<Chart />
```
## Setting Up Mdsvex
To make these work, you need to configure your SvelteKit project:
1. Install Mdsvex:
```bash
npm install -D mdsvex
```
2. Configure SvelteKit:
In your svelte.config.js:
```javascript
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { mdsvex } from 'mdsvex';
/** @type {import('mdsvex').MdsvexOptions} */
const mdsvexOptions = {
extensions: ['.svx'],
};
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: ['.svelte', '.svx'],
preprocess: [
vitePreprocess(),
mdsvex(mdsvexOptions),
],
kit: {
adapter: adapter()
}
};
export default config;
```
3. Create a Route for Markdown Files:
Place your .svx files in the src/routes directory or subdirectories, and SvelteKit will automatically handle them as routes.
These examples show how you can integrate Mdsvex into your Svelte application, combining the simplicity of Markdown with the interactivity of Svelte components. Remember, any Svelte component you want to use within Markdown must be exported from a .svelte file and imported in your .svx file.

View File

@@ -0,0 +1,66 @@
---
title: Extract Wisdom
date: 2024-01-01
description:
tags: [patterns, fabric]
---
# IDENTITY and PURPOSE
You extract surprising, insightful, and interesting information from text content. You are interested in insights related to the purpose and meaning of life, human flourishing, the role of technology in the future of humanity, artificial intelligence and its affect on humans, memes, learning, reading, books, continuous improvement, and similar topics.
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
# STEPS
- Extract a summary of the content in 25 words, including who is presenting and the content being discussed into a section called SUMMARY.
- Extract 20 to 50 of the most surprising, insightful, and/or interesting ideas from the input in a section called IDEAS:. If there are less than 50 then collect all of them. Make sure you extract at least 20.
- Extract 10 to 20 of the best insights from the input and from a combination of the raw input and the IDEAS above into a section called INSIGHTS. These INSIGHTS should be fewer, more refined, more insightful, and more abstracted versions of the best ideas in the content.
- Extract 15 to 30 of the most surprising, insightful, and/or interesting quotes from the input into a section called QUOTES:. Use the exact quote text from the input.
- Extract 15 to 30 of the most practical and useful personal habits of the speakers, or mentioned by the speakers, in the content into a section called HABITS. Examples include but aren't limited to: sleep schedule, reading habits, things they always do, things they always avoid, productivity tips, diet, exercise, etc.
- Extract 15 to 30 of the most surprising, insightful, and/or interesting valid facts about the greater world that were mentioned in the content into a section called FACTS:.
- Extract all mentions of writing, art, tools, projects and other sources of inspiration mentioned by the speakers into a section called REFERENCES. This should include any and all references to something that the speaker mentioned.
- Extract the most potent takeaway and recommendation into a section called ONE-SENTENCE TAKEAWAY. This should be a 15-word sentence that captures the most important essence of the content.
- Extract the 15 to 30 of the most surprising, insightful, and/or interesting recommendations that can be collected from the content into a section called RECOMMENDATIONS.
# OUTPUT INSTRUCTIONS
- Only output Markdown.
- Write the IDEAS bullets as exactly 15 words.
- Write the RECOMMENDATIONS bullets as exactly 15 words.
- Write the HABITS bullets as exactly 15 words.
- Write the FACTS bullets as exactly 15 words.
- Write the INSIGHTS bullets as exactly 15 words.
- Extract at least 25 IDEAS from the content.
- Extract at least 10 INSIGHTS from the content.
- Extract at least 20 items for the other output sections.
- Do not give warnings or notes; only output the requested sections.
- You use bulleted lists for output, not numbered lists.
- Do not repeat ideas, quotes, facts, or resources.
- Do not start items with the same opening words.
- Ensure you follow ALL these instructions when creating your output.
# INPUT
INPUT:

View File

@@ -0,0 +1,60 @@
---
title: Getting Started with SvelteKit
date: 2024-11-01
---
# Getting Started with SvelteKit
SvelteKit is a framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.
## Why SvelteKit?
- Zero-config setup
- Filesystem-based routing
- Server-side rendering
- Hot module replacement
```bash
npx sv create my-app
cd my-app
npm install
```
**Install SkeletonUI**
```bash
npm i -D @skeletonlabs/skeleton@next @skeletonlabs/skeleton-svelte@next
```
**Configure Tailwind CSS**
```tailwind.config
import type { Config } from 'tailwindcss';
import { skeleton, contentPath } from '@skeletonlabs/skeleton/plugin';
import * as themes from '@skeletonlabs/skeleton/themes';
export default {
content: [
'./src/**/*.{html,js,svelte,ts}',
contentPath(import.meta.url, 'svelte')
],
theme: {
extend: {},
},
plugins: [
skeleton({
// NOTE: each theme included will be added to your CSS bundle
themes: [ themes.cerberus, themes.rose ]
})
]
} satisfies Config
```
**Start the dev server**
```bash
npm run dev
```
Read more at https://svelte.dev, https://next.skeleton.dev/docs/get-started/installation/sveltekit, and https://www.skeleton.dev/docs/introduction

View File

@@ -0,0 +1,18 @@
---
title: Welcome to Your Blog
description: First post on my new SvelteKit blog
date: 2024-01-17
tags: [welcome, blog]
---
This is the first post of your new blog, powered by [SvelteKit](/posts/getting-started), [Obsidian](/obsidian), and [Fabric](/about). I'm excited to share this project with you, and I hope you find it useful for your own writing and experiences.
This part of the application is edited in <a href="http://localhost:5173/obsidian" name="Obsidian">Obsidian</a>.
## What to Expect
- Updates on Incorporating Fabric into your workflow
- How to use Obsidian to manage you notes and workflows
- How to use Fabric and Obsidian to write and publish
- More ways to use Obsidian and Fabric together!
Stay tuned for more content!

View File

@@ -0,0 +1,7 @@
---
title: Your Title Here
date:
description: Post description
updated:
---
{{Content}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -0,0 +1,50 @@
<script lang="ts">
import { formatDistance } from 'date-fns';
import TagList from '$components/ui/tag-list/TagList.svelte';
/** @type {string} */
export let title;
/** @type {string} */
export let date;
/** @type {string} */
export let description;
/** @type {string} */
export let tags = [];
/** @type {string}*/
export let updated;
/** @type {string}**/
export let reference;
</script>
<article class="prose prose-slate mx-auto max-w-3xl dark:prose-invert py-12">
<header class="mb-8 not-prose">
{#if title}
<h1 class="mb-2 text-4xl font-bold">{title}</h1>
{/if}
{#if description}
<p class="mb-4 text-lg text-muted-foreground">{description}</p>
{/if}
{#if date}
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
<time datetime={date}>{formatDistance(new Date(date), new Date(), { addSuffix: true })}</time>
{#if tags?.length}
<span class="text-xs"></span>
<TagList {tags} className="flex-1" />
{/if}
{#if updated}
<span class="text-xs"></span>
<time datetime={updated}>Updated {formatDistance(new Date(updated), new Date(), { addSuffix: true })}</time>
{/if}
{#if reference}
<span class="text-xs"></span>
<a href={reference}>Reference</a>
{/if}
</div>
{/if}
</header>
<div class="mt-8">
<slot />
</div>
</article>

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