mirror of
https://github.com/danielmiessler/fabric.git
synced 2026-09-10 15:46:39 -04:00
Compare commits
78 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b682dad740 | ||
|
|
e91b4f0af1 | ||
|
|
f8bc07f2d7 | ||
|
|
8db08ad20f | ||
|
|
6b0914d1bb | ||
|
|
3ca2133819 | ||
|
|
f141b68f12 | ||
|
|
8dce453ed7 | ||
|
|
4637fa1a22 | ||
|
|
a3d1dc2cae | ||
|
|
00f56a76e0 | ||
|
|
e9b32ba546 | ||
|
|
4835506879 | ||
|
|
852ffd8468 | ||
|
|
fc0b2c0294 | ||
|
|
a8afe60608 | ||
|
|
892c70a8bb | ||
|
|
63846b523c | ||
|
|
f37c71f9f7 | ||
|
|
cc2a9d39d6 | ||
|
|
95c5b7d6ba | ||
|
|
b136259cfb | ||
|
|
52f63d0795 | ||
|
|
2c510971dd | ||
|
|
b185a5c790 | ||
|
|
8fd6ce2418 | ||
|
|
8aeaa1bb5e | ||
|
|
3ad89a3516 | ||
|
|
cd54c1a82f | ||
|
|
435b89d936 | ||
|
|
c10bd5a380 | ||
|
|
374d2dc967 | ||
|
|
54a7775e5b | ||
|
|
19b87b33a2 | ||
|
|
1b907fe318 | ||
|
|
9b724f8cec | ||
|
|
733a1810bf | ||
|
|
338b89cfe9 | ||
|
|
c9211929c4 | ||
|
|
9614bf2f16 | ||
|
|
2580d130c0 | ||
|
|
0cb6a9facc | ||
|
|
6627337e54 | ||
|
|
56d330f5e8 | ||
|
|
829fa3bd07 | ||
|
|
28273a0533 | ||
|
|
ab64555925 | ||
|
|
cb340b6b02 | ||
|
|
32ff0b5591 | ||
|
|
2b1b31ca3f | ||
|
|
b90a07bc19 | ||
|
|
a4b89f65c2 | ||
|
|
9e92bca148 | ||
|
|
fe2fbe9cff | ||
|
|
c9fa92bf03 | ||
|
|
7a638ff3cc | ||
|
|
bd4c9c93d2 | ||
|
|
b12a1ef1eb | ||
|
|
8135a11606 | ||
|
|
fba2543895 | ||
|
|
e8fd77c40b | ||
|
|
9f38ee0eff | ||
|
|
1b1feea2b5 | ||
|
|
737c3ac372 | ||
|
|
e25005c8d5 | ||
|
|
9e685112af | ||
|
|
e65c35d63e | ||
|
|
d8e934df22 | ||
|
|
7f2e663cb9 | ||
|
|
01f88e5c2b | ||
|
|
9730808119 | ||
|
|
6cef8c10ad | ||
|
|
cd8c249744 | ||
|
|
080311effc | ||
|
|
f95bff568f | ||
|
|
ec0b7246c4 | ||
|
|
3b03c17f9a | ||
|
|
175772e8a0 |
127
CHANGELOG.md
127
CHANGELOG.md
|
|
@ -1,5 +1,132 @@
|
|||
# Changelog
|
||||
|
||||
## v1.4.478 (2026-09-06)
|
||||
|
||||
### PR [#2216](https://github.com/danielmiessler/Fabric/pull/2216) by [ctbaum](https://github.com/ctbaum): fix: enable raw mode for GPT-6 models
|
||||
|
||||
- Fix: enable raw mode for GPT-6 models
|
||||
|
||||
## v1.4.477 (2026-09-03)
|
||||
|
||||
### PR [#2211](https://github.com/danielmiessler/Fabric/pull/2211) by [ksylvan](https://github.com/ksylvan): fix: prevent pattern loader temporary directory leaks
|
||||
|
||||
- Prevent pattern loader leaks by lazily creating temporary directories during database population and cleaning them up after successful or failed downloads.
|
||||
- Add regression tests for lazy directory creation and cleanup.
|
||||
|
||||
## v1.4.476 (2026-09-03)
|
||||
|
||||
### PR [#2210](https://github.com/danielmiessler/Fabric/pull/2210) by [ksylvan](https://github.com/ksylvan): feat: add Pzero as an OpenAI-compatible AI provider
|
||||
|
||||
- Added Pzero as an OpenAI-compatible AI provider.
|
||||
- Registered Pzero with its OpenAI-compatible API base URL.
|
||||
- Added Pzero to the README’s list of supported AI providers.
|
||||
|
||||
## v1.4.475 (2026-09-03)
|
||||
|
||||
### PR [#2209](https://github.com/danielmiessler/Fabric/pull/2209) by [kadiryildiz283](https://github.com/kadiryildiz283): feat(i18n): add Turkish (tr) translation
|
||||
|
||||
- Feat(i18n): add Turkish (tr) translation
|
||||
|
||||
## v1.4.474 (2026-09-02)
|
||||
|
||||
### PR [#2206](https://github.com/danielmiessler/Fabric/pull/2206) by [ksylvan](https://github.com/ksylvan): fix: confine storage names and authenticate Ollama serve
|
||||
|
||||
- Reject unsafe cross-platform storage names and directory traversal attempts.
|
||||
- Confine symlink targets to configured filesystem storage directories.
|
||||
- Require API keys for non-loopback server bindings and authenticate Ollama routes.
|
||||
- Validate chat pattern, context, and session names early while preventing internal filesystem details from leaking through client errors.
|
||||
- Default the REST server to loopback port 8080 and add regression coverage for traversal, symlink, and authentication security.
|
||||
|
||||
## v1.4.473 (2026-08-29)
|
||||
|
||||
### PR [#2203](https://github.com/danielmiessler/Fabric/pull/2203) by [pacocartones](https://github.com/pacocartones) and [ksylvan](https://github.com/ksylvan): fix(web): preserve multi-byte UTF-8 split across SSE chunks in chat stream
|
||||
|
||||
- Fix: preserve split UTF-8 characters in streaming responses
|
||||
- Decode API response chunks with persistent streaming state
|
||||
- Reuse streaming decoder when inspecting chat backend events
|
||||
|
||||
## v1.4.472 (2026-08-29)
|
||||
|
||||
### PR [#2198](https://github.com/danielmiessler/Fabric/pull/2198) by [cuihuan](https://github.com/cuihuan): feat(providers): add Synthorai as an OpenAI-compatible provider
|
||||
|
||||
- Feat(providers): add Synthorai as an OpenAI-compatible provider
|
||||
|
||||
### Direct commits
|
||||
|
||||
- Chore: delete generate_changelog noise in README.
|
||||
|
||||
## v1.4.471 (2026-08-28)
|
||||
|
||||
### PR [#2200](https://github.com/danielmiessler/Fabric/pull/2200) by [ksylvan](https://github.com/ksylvan): fix: persist Codex OAuth tokens after refresh
|
||||
|
||||
- Persist refreshed Codex OAuth tokens securely across processes, writing rotated credentials atomically with cross-process locks.
|
||||
- Reload stored tokens before refresh and return valid in-memory tokens first, preventing stale disk values from overwriting fresh credentials.
|
||||
- Reuse valid refresh tokens before launching interactive authentication, requiring account identifiers and unexpired tokens for fast-path reuse.
|
||||
- Preserve existing environment values, save environment files atomically while holding locks, and enforce secret-only permissions.
|
||||
- Reactivate configured vendors and surface configuration and provider failures immediately, with localized Codex prompts and persistence errors.
|
||||
|
||||
## v1.4.470 (2026-08-04)
|
||||
|
||||
### PR [#2186](https://github.com/danielmiessler/Fabric/pull/2186) by [giodamelio](https://github.com/giodamelio): Update Nixpkgs version for newer Go version
|
||||
|
||||
- Updated the Nixpkgs version to provide a Go release that satisfies the `go.mod` requirement of Go >= 1.26.0, as the previously pinned Nixpkgs only shipped Go 1.25.4.
|
||||
|
||||
## v1.4.469 (2026-08-03)
|
||||
|
||||
### PR [#2185](https://github.com/danielmiessler/Fabric/pull/2185) by [ksylvan](https://github.com/ksylvan): feat(web): replace PDF.js pipeline with pdf-inspector WASM worker
|
||||
|
||||
- Replaced the PDF.js processing pipeline with the new `pdf-inspector.worker.ts`, backed by `@firecrawl/pdf-inspector-wasm`. The worker initializes WASM one time and processes transferred ArrayBuffers.
|
||||
- Added worker error handling. A worker crash now rejects all pending requests with a clear error and terminates the worker. The next request starts a new worker.
|
||||
- Fixed the file attachment behavior. The app now parses each file and stores its content without a chat request. One submit sends exactly one `streamChat` call, not one call for each attached file.
|
||||
- Removed the legacy PDF dependency chain: `pdf-to-markdown-core`, `pdfjs-dist`, `pdf-config.ts`, and the related build configuration. `@firecrawl/pdf-inspector-wasm` is now the only PDF dependency.
|
||||
- Simplified the worker boundary in follow-up refactors: merged redundant methods, removed dead code, and inlined a single-use interface. The full test suite continued to pass.
|
||||
|
||||
## v1.4.468 (2026-08-02)
|
||||
|
||||
### PR [#2182](https://github.com/danielmiessler/Fabric/pull/2182) by [drawliin](https://github.com/drawliin): fix(ollama): close stream channel on errors
|
||||
|
||||
- Fix(ollama): close stream channel on errors
|
||||
|
||||
## v1.4.467 (2026-07-31)
|
||||
|
||||
### PR [#2171](https://github.com/danielmiessler/Fabric/pull/2171) by [OdinKral](https://github.com/OdinKral): feat(scripts): add pattern/maintenance audit script
|
||||
|
||||
- Adds `scripts/audit-patterns.sh`, a dependency-free audit tool for the patterns library that detects thin patterns (under 15 lines), bloated patterns (over 50 KB), stale hardcoded model references, missing `INPUT` sections, i18n key gaps, and shell completion gaps. The script is read-only and exits with code `0` by default, or `1` when run with `--strict` for CI integration.
|
||||
|
||||
## v1.4.466 (2026-07-30)
|
||||
|
||||
### PR [#2116](https://github.com/danielmiessler/Fabric/pull/2116) by [ksylvan](https://github.com/ksylvan) and [dependabot](https://github.com/apps/dependabot): fix(web): repair the chat page and modernize the build toolchain
|
||||
|
||||
- Modernized the web stack by upgrading Tailwind, Skeleton, SvelteKit, Vite, and supporting dependencies.
|
||||
- Replaced removed Skeleton components with compatible local implementations to restore UI functionality.
|
||||
- Improved pre-stream chat error reporting to prevent duplication of streamed errors, and hardened vendor list parsing to return empty model arrays on malformed input.
|
||||
- Fixed stream completion crashes, normalized displayed error messages, corrected toast stacking order, and added warning notification support.
|
||||
- Restored linting, formatting, testing, and pnpm override configuration, and preserved custom themes through the Tailwind CSS-based configuration migration.
|
||||
|
||||
### PR [#2178](https://github.com/danielmiessler/Fabric/pull/2178) by [OdinKral](https://github.com/OdinKral) and [ksylvan](https://github.com/ksylvan): feat(patterns): add generate_frontmatter for PKM/Obsidian users
|
||||
|
||||
- Add `generate_frontmatter` to conversion, extraction, and writing categories
|
||||
- Document PKM-ready YAML metadata fields in pattern explanations
|
||||
- Register pattern descriptions and extracts for suggestion workflows
|
||||
- Credit both contributors in incoming changelog entry
|
||||
|
||||
## v1.4.465 (2026-07-29)
|
||||
|
||||
### PR [#2180](https://github.com/danielmiessler/Fabric/pull/2180) by [ksylvan](https://github.com/ksylvan): fix: include Grok in localized --search help text
|
||||
|
||||
- Fix: Updated all 11 locale entries in the i18n message catalog to include "Grok" in the `enable_web_search_tool` help text, ensuring Grok (xAI) web search support is discoverable via `fabric --help`. Also updates the generated README help block to match. No functional change.
|
||||
|
||||
## v1.4.464 (2026-07-29)
|
||||
|
||||
### PR [#2179](https://github.com/danielmiessler/Fabric/pull/2179) by [ksylvan](https://github.com/ksylvan): fix: declare completion arguments and synchronize CLI help
|
||||
|
||||
- Fix: declare completion arguments and synchronize CLI help
|
||||
- Declare Fish completion arguments for dynamic and fixed values
|
||||
- Enable filtered file suggestions for supported path options
|
||||
- Add Spotify, transcription, metadata, and wire-debug completion options
|
||||
- Include Grok among providers supporting web search completion
|
||||
- Keep the input/output token detail in the show-metadata description
|
||||
|
||||
## v1.4.463 (2026-07-28)
|
||||
|
||||
### PR [#2168](https://github.com/danielmiessler/Fabric/pull/2168) by [ksylvan](https://github.com/ksylvan): fix: complete localized setup and error messages for supported locales
|
||||
|
|
|
|||
55
README.md
55
README.md
|
|
@ -374,7 +374,9 @@ Fabric supports a wide range of AI providers:
|
|||
- Mistral
|
||||
- Novita AI
|
||||
- OpenRouter
|
||||
- Pzero
|
||||
- SiliconCloud
|
||||
- Synthorai
|
||||
- Together
|
||||
- Venice AI
|
||||
- Z AI
|
||||
|
|
@ -664,37 +666,41 @@ Application Options:
|
|||
-T, --topp= Set top P (default: 0.9)
|
||||
-s, --stream Stream
|
||||
-P, --presencepenalty= Set presence penalty (default: 0.0)
|
||||
-r, --raw Use the defaults of the model without sending chat options
|
||||
(temperature, top_p, etc.). Only affects OpenAI-compatible providers.
|
||||
Anthropic models always use smart parameter selection to comply with
|
||||
model-specific requirements.
|
||||
-r, --raw Use the defaults of the model without sending chat options (temperature,
|
||||
top_p, etc.). Only affects OpenAI-compatible providers. Anthropic models
|
||||
always use smart parameter selection to comply with model-specific
|
||||
requirements.
|
||||
-F, --frequencypenalty= Set frequency penalty (default: 0.0)
|
||||
-l, --listpatterns List all patterns
|
||||
--readpattern= Print the contents of the named pattern to the terminal
|
||||
-L, --listmodels List all available models
|
||||
-x, --listcontexts List all contexts
|
||||
-X, --listsessions List all sessions
|
||||
-U, --updatepatterns Update patterns
|
||||
-c, --copy Copy to clipboard
|
||||
-m, --model= Choose model
|
||||
-V, --vendor= Specify vendor for chosen model (e.g., -V "LM Studio" -m openai/gpt-oss-20b)
|
||||
-V, --vendor= Specify vendor for the selected model (e.g., -V "LM Studio" -m
|
||||
openai/gpt-oss-20b)
|
||||
--modelContextLength= Model context length (only affects ollama)
|
||||
-o, --output= Output to file
|
||||
--output-session Output the entire session (also a temporary one) to the output file
|
||||
-n, --latest= Number of latest patterns to list (default: 0)
|
||||
-n, --latest= Number of latest patterns to list
|
||||
-d, --changeDefaultModel Change default model
|
||||
-y, --youtube= YouTube video or play list "URL" to grab transcript, comments from it
|
||||
and send to chat or print it put to the console and store it in the
|
||||
output file
|
||||
-y, --youtube= YouTube video or play list "URL" to grab transcript, comments from it and
|
||||
send to chat or print it put to the console and store it in the output file
|
||||
--playlist Prefer playlist over video if both ids are present in the URL
|
||||
--transcript Grab transcript from YouTube video and send to chat (it is used per
|
||||
default).
|
||||
--transcript-with-timestamps Grab transcript from YouTube video with timestamps and send to chat
|
||||
--visual Extract visual data from video using OCR and FFmpeg
|
||||
--visual-sensitivity Tolerance for FFmpeg scene detection (0.0 - 1.0)
|
||||
--visual-fps Extract a specific number of frames per second instead of using scene detection
|
||||
--visual-sensitivity= Tolerance for FFmpeg scene detection (0.0 - 1.0) (default: 0.4)
|
||||
--visual-fps= Extract a specific number of frames per second instead of using scene
|
||||
detection
|
||||
--comments Grab comments from YouTube video and send to chat
|
||||
--metadata Output video metadata
|
||||
-g, --language= Specify the Language Code for the chat, e.g. -g=en -g=zh
|
||||
--yt-dlp-args= Additional arguments to pass to yt-dlp (e.g. '--cookies-from-browser brave')
|
||||
--spotify= Spotify podcast or episode URL to grab metadata from and send to chat
|
||||
-g, --language= Specify the Language Code for the chat, e.g. -g=en -g=zh -g=pt-BR -g=pt-PT
|
||||
-u, --scrape_url= Scrape website URL to markdown using Jina AI
|
||||
-q, --scrape_question= Search question using Jina AI
|
||||
-e, --seed= Seed to be used for LMM generation
|
||||
|
|
@ -719,29 +725,31 @@ Application Options:
|
|||
--liststrategies List all strategies
|
||||
--listvendors List all vendors
|
||||
--shell-complete-list Output raw list without headers/formatting (for shell completion)
|
||||
--search Enable web search tool for supported models (Anthropic, OpenAI, Gemini)
|
||||
--search Enable web search tool for supported models (Anthropic, OpenAI, Gemini, Grok)
|
||||
--search-location= Set location for web search results (e.g., 'America/Los_Angeles')
|
||||
--image-file= Save generated image to specified file path (e.g., 'output.png')
|
||||
--image-size= Image dimensions: 1024x1024, 1536x1024, 1024x1536, auto (default: auto)
|
||||
--image-quality= Image quality: low, medium, high, auto (default: auto)
|
||||
--image-compression= Compression level 0-100 for JPEG/WebP formats (default: not set)
|
||||
--image-background= Background type: opaque, transparent (default: opaque, only for
|
||||
PNG/WebP)
|
||||
--image-background= Background type: opaque, transparent (default: opaque, only for PNG/WebP)
|
||||
--suppress-think Suppress text enclosed in thinking tags
|
||||
--think-start-tag= Start tag for thinking sections (default: <think>)
|
||||
--think-end-tag= End tag for thinking sections (default: </think>)
|
||||
--disable-responses-api Disable OpenAI Responses API (default: false)
|
||||
--voice= TTS voice name for supported models (e.g., Kore, Charon, Puck)
|
||||
(default: Kore)
|
||||
--transcribe-file= Audio or video file to transcribe
|
||||
--transcribe-model= Model to use for transcription (separate from chat model)
|
||||
--split-media-file Split audio/video files larger than 25MB using ffmpeg
|
||||
--voice= TTS voice name for supported models (e.g., Kore, Charon, Puck) (default:
|
||||
Kore)
|
||||
--list-gemini-voices List all available Gemini TTS voices
|
||||
--list-transcription-models List all available transcription models
|
||||
--notification Send desktop notification when command completes
|
||||
--notification-command= Custom command to run for notifications (overrides built-in
|
||||
notifications)
|
||||
--yt-dlp-args= Additional arguments to pass to yt-dlp (e.g. '--cookies-from-browser brave')
|
||||
--thinking= Set reasoning/thinking level (e.g., off, low, medium, high, or
|
||||
numeric tokens for Anthropic or Google Gemini)
|
||||
--notification-command= Custom command to run for notifications (overrides built-in notifications)
|
||||
--thinking= Set reasoning/thinking level (e.g., off, low, medium, high, or numeric
|
||||
tokens for Anthropic or Google Gemini)
|
||||
--show-metadata Print metadata (input/output tokens) to stderr
|
||||
--debug= Set debug level (0: off, 1: basic, 2: detailed, 3: trace)
|
||||
--debug= Set debug level (0=off, 1=basic, 2=detailed, 3=trace, 4=wire)
|
||||
|
||||
Help Options:
|
||||
-h, --help Show this help message
|
||||
```
|
||||
|
|
@ -754,6 +762,7 @@ Use the `--debug` flag to control runtime logging:
|
|||
- `1`: basic debug info
|
||||
- `2`: detailed debugging
|
||||
- `3`: trace level
|
||||
- `4`: wire level (full request and response bodies)
|
||||
|
||||
### Dry Run Mode
|
||||
|
||||
|
|
|
|||
|
|
@ -358,7 +358,7 @@ fabric --setup
|
|||
|
||||
**OpenAI 兼容供应商:**
|
||||
|
||||
- Abacus、AIML、Cerebras、DeepSeek、DigitalOcean、GitHub Models、GrokAI、Groq、Langdock、LiteLLM、MiniMax、Mistral、Novita AI、OpenRouter、SiliconCloud、Together、Venice AI、Z AI
|
||||
- Abacus、AIML、Cerebras、DeepSeek、DigitalOcean、GitHub Models、GrokAI、Groq、Langdock、LiteLLM、MiniMax、Mistral、Novita AI、OpenRouter、SiliconCloud、Synthorai、Together、Venice AI、Z AI
|
||||
|
||||
运行 `fabric --setup` 配置首选供应商,或使用 `fabric --listvendors` 查看所有可用供应商。
|
||||
|
||||
|
|
@ -480,6 +480,7 @@ pbpaste | fabric --pattern extract_wisdom
|
|||
- `1`:基本调试信息
|
||||
- `2`:详细调试
|
||||
- `3`:追踪级别
|
||||
- `4`:wire 级别(完整的请求和响应内容)
|
||||
|
||||
### 演习模式
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
package main
|
||||
|
||||
var version = "v1.4.463"
|
||||
var version = "v1.4.478"
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ Create a `.env` file next to the `generate_changelog` binary:
|
|||
|
||||
```bash
|
||||
GITHUB_TOKEN=your_github_token_here
|
||||
FABRIC_CHANGELOG_SUMMARIZE_MODEL=claude-sonnet-4-20250514
|
||||
FABRIC_CHANGELOG_SUMMARIZE_MODEL=gpt-5.6-sol
|
||||
```
|
||||
|
||||
The tool automatically loads `.env` files for convenient configuration management.
|
||||
|
|
@ -226,8 +226,8 @@ The tool can generate AI-powered summaries using Fabric for more polished, profe
|
|||
# Enable AI summarization
|
||||
generate_changelog --ai-summarize
|
||||
|
||||
# Custom model (default: claude-sonnet-4-20250514)
|
||||
FABRIC_CHANGELOG_SUMMARIZE_MODEL=claude-opus-4 generate_changelog --ai-summarize
|
||||
# Custom model (default: claude-opus-5)
|
||||
FABRIC_CHANGELOG_SUMMARIZE_MODEL=gpt-5.6-sol generate_changelog --ai-summarize
|
||||
```
|
||||
|
||||
### AI Summary Features
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -7,7 +7,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
const DefaultSummarizeModel = "claude-sonnet-4-5"
|
||||
const DefaultSummarizeModel = "claude-opus-5"
|
||||
const MinContentLength = 256 // Minimum content length to consider for summarization
|
||||
|
||||
const prompt = `# ROLE
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ _fabric() {
|
|||
'(-U --updatepatterns)'{-U,--updatepatterns}'[Update patterns]' \
|
||||
'(-c --copy)'{-c,--copy}'[Copy to clipboard]' \
|
||||
'(-m --model)'{-m,--model}'[Choose model]:model:_fabric_models' \
|
||||
'(-V --vendor)'{-V,--vendor}'[Specify vendor for chosen model (e.g., -V "LM Studio" -m openai/gpt-oss-20b)]:vendor:_fabric_vendors' \
|
||||
'(-V --vendor)'{-V,--vendor}'[Specify vendor for the selected model (e.g., -V "LM Studio" -m openai/gpt-oss-20b)]:vendor:_fabric_vendors' \
|
||||
'(--modelContextLength)--modelContextLength[Model context length (only affects ollama)]:length:' \
|
||||
'(-o --output)'{-o,--output}'[Output to file]:file:_files' \
|
||||
'(--output-session)--output-session[Output the entire session to the output file]' \
|
||||
|
|
@ -126,7 +126,7 @@ _fabric() {
|
|||
'(--api-key)--api-key[API key used to secure server routes]:api-key:' \
|
||||
'(--config)--config[Path to YAML config file]:config file:_files -g "*.yaml *.yml"' \
|
||||
'(--version)--version[Print current version]' \
|
||||
'(--search)--search[Enable web search tool for supported models (Anthropic, OpenAI, Gemini)]' \
|
||||
'(--search)--search[Enable web search tool for supported models (Anthropic, OpenAI, Gemini, Grok)]' \
|
||||
'(--search-location)--search-location[Set location for web search results]:location:' \
|
||||
'(--image-file)--image-file[Save generated image to specified file path]:image file:_files -g "*.png *.webp *.jpeg *.jpg"' \
|
||||
'(--image-size)--image-size[Image dimensions]:size:(1024x1024 1536x1024 1024x1536 auto)' \
|
||||
|
|
@ -141,6 +141,7 @@ _fabric() {
|
|||
'(--listvendors)--listvendors[List all vendors]' \
|
||||
'(--voice)--voice[TTS voice name for supported models]:voice:_fabric_gemini_voices' \
|
||||
'(--list-gemini-voices)--list-gemini-voices[List all available Gemini TTS voices]' \
|
||||
'(--list-transcription-models)--list-transcription-models[List all available transcription models]' \
|
||||
'(--shell-complete-list)--shell-complete-list[Output raw list without headers/formatting (for shell completion)]' \
|
||||
'(--suppress-think)--suppress-think[Suppress text enclosed in thinking tags]' \
|
||||
'(--think-start-tag)--think-start-tag[Start tag for thinking sections (default: <think>)]:start tag:' \
|
||||
|
|
@ -149,6 +150,7 @@ _fabric() {
|
|||
'(--transcribe-file)--transcribe-file[Audio or video file to transcribe]:audio file:_files -g "*.mp3 *.mp4 *.mpeg *.mpga *.m4a *.wav *.webm"' \
|
||||
'(--transcribe-model)--transcribe-model[Model to use for transcription (separate from chat model)]:transcribe model:_fabric_transcription_models' \
|
||||
'(--split-media-file)--split-media-file[Split audio/video files larger than 25MB using ffmpeg]' \
|
||||
'(--show-metadata)--show-metadata[Print metadata (input/output tokens) to stderr]' \
|
||||
'(--debug)--debug[Set debug level (0=off, 1=basic, 2=detailed, 3=trace, 4=wire)]:debug level:(0 1 2 3 4)' \
|
||||
'(--notification)--notification[Send desktop notification when command completes]' \
|
||||
'(--notification-command)--notification-command[Custom command to run for notifications]:notification command:' \
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ _fabric() {
|
|||
fi
|
||||
|
||||
# Define all possible options/flags
|
||||
local opts="--pattern -p --variable -v --context -C --session --attachment -a --setup -S --temperature -t --topp -T --stream -s --presencepenalty -P --raw -r --frequencypenalty -F --listpatterns -l --readpattern --listmodels -L --listcontexts -x --listsessions -X --updatepatterns -U --copy -c --model -m --vendor -V --modelContextLength --output -o --output-session --latest -n --changeDefaultModel -d --youtube -y --playlist --transcript --transcript-with-timestamps --visual --visual-sensitivity --visual-fps --comments --metadata --yt-dlp-args --language -g --scrape_url -u --scrape_question -q --seed -e --thinking --wipecontext -w --wipesession -W --printcontext --printsession --readability --input-has-vars --no-variable-replacement --dry-run --serve --serveOllama --address --api-key --config --search --search-location --image-file --image-size --image-quality --image-compression --image-background --suppress-think --think-start-tag --think-end-tag --disable-responses-api --transcribe-file --transcribe-model --split-media-file --voice --list-gemini-voices --notification --notification-command --debug --version --listextensions --addextension --rmextension --strategy --liststrategies --listvendors --shell-complete-list --help -h"
|
||||
local opts="--pattern -p --variable -v --context -C --session --attachment -a --setup -S --temperature -t --topp -T --stream -s --presencepenalty -P --raw -r --frequencypenalty -F --listpatterns -l --readpattern --listmodels -L --listcontexts -x --listsessions -X --updatepatterns -U --copy -c --model -m --vendor -V --modelContextLength --output -o --output-session --latest -n --changeDefaultModel -d --youtube -y --playlist --transcript --transcript-with-timestamps --visual --visual-sensitivity --visual-fps --comments --metadata --yt-dlp-args --spotify --language -g --scrape_url -u --scrape_question -q --seed -e --thinking --wipecontext -w --wipesession -W --printcontext --printsession --readability --input-has-vars --no-variable-replacement --dry-run --serve --serveOllama --address --api-key --config --search --search-location --image-file --image-size --image-quality --image-compression --image-background --suppress-think --think-start-tag --think-end-tag --disable-responses-api --transcribe-file --transcribe-model --split-media-file --voice --list-gemini-voices --list-transcription-models --notification --notification-command --show-metadata --debug --version --listextensions --addextension --rmextension --strategy --liststrategies --listvendors --shell-complete-list --help -h"
|
||||
|
||||
# Helper function for dynamic completions
|
||||
_fabric_get_list() {
|
||||
|
|
|
|||
|
|
@ -53,57 +53,69 @@ function __fabric_get_transcription_models
|
|||
end
|
||||
|
||||
# Main completion function
|
||||
#
|
||||
# Options that take a value must declare it. Use -x when only the listed values
|
||||
# are valid, and -r when a file path is also valid. Fish ignores the -a list of
|
||||
# an option that declares neither, and shows no suggestion after the option.
|
||||
function __fabric_register_completions
|
||||
set cmd $argv[1]
|
||||
complete -c $cmd -f
|
||||
|
||||
# Flag completions with arguments
|
||||
complete -c $cmd -s p -l pattern -d "Choose a pattern from the available patterns" -a "(__fabric_get_patterns)"
|
||||
complete -c $cmd -l readpattern -d "Print the contents of the named pattern to the terminal" -a "(__fabric_get_patterns)"
|
||||
complete -c $cmd -s v -l variable -d "Values for pattern variables, e.g. -v=#role:expert -v=#points:30"
|
||||
complete -c $cmd -s C -l context -d "Choose a context from the available contexts" -a "(__fabric_get_contexts)"
|
||||
complete -c $cmd -l session -d "Choose a session from the available sessions" -a "(__fabric_get_sessions)"
|
||||
complete -c $cmd -s a -l attachment -d "Attachment path or URL (e.g. for OpenAI image recognition messages)" -r
|
||||
complete -c $cmd -s t -l temperature -d "Set temperature (default: 0.7)"
|
||||
complete -c $cmd -s T -l topp -d "Set top P (default: 0.9)"
|
||||
complete -c $cmd -s P -l presencepenalty -d "Set presence penalty (default: 0.0)"
|
||||
complete -c $cmd -s F -l frequencypenalty -d "Set frequency penalty (default: 0.0)"
|
||||
complete -c $cmd -s m -l model -d "Choose model" -a "(__fabric_get_models)"
|
||||
complete -c $cmd -s V -l vendor -d "Specify vendor for chosen model (e.g., -V \"LM Studio\" -m openai/gpt-oss-20b)" -a "(__fabric_get_vendors)"
|
||||
complete -c $cmd -l modelContextLength -d "Model context length (only affects ollama)"
|
||||
complete -c $cmd -s o -l output -d "Output to file" -r
|
||||
complete -c $cmd -s n -l latest -d "Number of latest patterns to list (default: 0)"
|
||||
complete -c $cmd -s y -l youtube -d "YouTube video or play list URL to grab transcript, comments from it"
|
||||
complete -c $cmd -l visual-sensitivity -d "Tolerance for FFmpeg scene detection (0.0 - 1.0)"
|
||||
complete -c $cmd -l visual-fps -d "Extract a specific number of frames per second instead of using scene detection"
|
||||
complete -c $cmd -s g -l language -d "Specify the Language Code for the chat, e.g. -g=en -g=zh"
|
||||
complete -c $cmd -s u -l scrape_url -d "Scrape website URL to markdown using Jina AI"
|
||||
complete -c $cmd -s q -l scrape_question -d "Search question using Jina AI"
|
||||
complete -c $cmd -s e -l seed -d "Seed to be used for LMM generation"
|
||||
complete -c $cmd -l thinking -d "Set reasoning/thinking level" -a "off low medium high"
|
||||
complete -c $cmd -s w -l wipecontext -d "Wipe context" -a "(__fabric_get_contexts)"
|
||||
complete -c $cmd -s W -l wipesession -d "Wipe session" -a "(__fabric_get_sessions)"
|
||||
complete -c $cmd -l printcontext -d "Print context" -a "(__fabric_get_contexts)"
|
||||
complete -c $cmd -l printsession -d "Print session" -a "(__fabric_get_sessions)"
|
||||
complete -c $cmd -l address -d "The address to bind the REST API (default: :8080)"
|
||||
complete -c $cmd -l api-key -d "API key used to secure server routes"
|
||||
complete -c $cmd -l config -d "Path to YAML config file" -r -a "*.yaml *.yml"
|
||||
complete -c $cmd -l search-location -d "Set location for web search results (e.g., 'America/Los_Angeles')"
|
||||
complete -c $cmd -l image-file -d "Save generated image to specified file path (e.g., 'output.png')" -r -a "*.png *.webp *.jpeg *.jpg"
|
||||
complete -c $cmd -l image-size -d "Image dimensions: 1024x1024, 1536x1024, 1024x1536, auto (default: auto)" -a "1024x1024 1536x1024 1024x1536 auto"
|
||||
complete -c $cmd -l image-quality -d "Image quality: low, medium, high, auto (default: auto)" -a "low medium high auto"
|
||||
complete -c $cmd -l image-compression -d "Compression level 0-100 for JPEG/WebP formats (default: not set)" -r
|
||||
complete -c $cmd -l image-background -d "Background type: opaque, transparent (default: opaque, only for PNG/WebP)" -a "opaque transparent"
|
||||
complete -c $cmd -l addextension -d "Register a new extension from config file path" -r -a "*.yaml *.yml"
|
||||
complete -c $cmd -l rmextension -d "Remove a registered extension by name" -a "(__fabric_get_extensions)"
|
||||
complete -c $cmd -l strategy -d "Choose a strategy from the available strategies" -a "(__fabric_get_strategies)"
|
||||
complete -c $cmd -l think-start-tag -d "Start tag for thinking sections (default: <think>)"
|
||||
complete -c $cmd -l think-end-tag -d "End tag for thinking sections (default: </think>)"
|
||||
complete -c $cmd -l voice -d "TTS voice name for supported models (e.g., Kore, Charon, Puck)" -a "(__fabric_get_gemini_voices)"
|
||||
complete -c $cmd -l transcribe-file -d "Audio or video file to transcribe" -r -a "*.mp3 *.mp4 *.mpeg *.mpga *.m4a *.wav *.webm"
|
||||
complete -c $cmd -l transcribe-model -d "Model to use for transcription (separate from chat model)" -a "(__fabric_get_transcription_models)"
|
||||
complete -c $cmd -l debug -d "Set debug level (0=off, 1=basic, 2=detailed, 3=trace, 4=wire)" -a "0 1 2 3 4"
|
||||
complete -c $cmd -l notification-command -d "Custom command to run for notifications (overrides built-in notifications)"
|
||||
# Options that take a value from a dynamic list
|
||||
complete -c $cmd -s p -l pattern -x -d "Choose a pattern from the available patterns" -a "(__fabric_get_patterns)"
|
||||
complete -c $cmd -l readpattern -x -d "Print the contents of the named pattern to the terminal" -a "(__fabric_get_patterns)"
|
||||
complete -c $cmd -s C -l context -x -d "Choose a context from the available contexts" -a "(__fabric_get_contexts)"
|
||||
complete -c $cmd -l session -x -d "Choose a session from the available sessions" -a "(__fabric_get_sessions)"
|
||||
complete -c $cmd -s m -l model -x -d "Choose model" -a "(__fabric_get_models)"
|
||||
complete -c $cmd -s V -l vendor -x -d "Specify vendor for the selected model (e.g., -V \"LM Studio\" -m openai/gpt-oss-20b)" -a "(__fabric_get_vendors)"
|
||||
complete -c $cmd -s w -l wipecontext -x -d "Wipe context" -a "(__fabric_get_contexts)"
|
||||
complete -c $cmd -s W -l wipesession -x -d "Wipe session" -a "(__fabric_get_sessions)"
|
||||
complete -c $cmd -l printcontext -x -d "Print context" -a "(__fabric_get_contexts)"
|
||||
complete -c $cmd -l printsession -x -d "Print session" -a "(__fabric_get_sessions)"
|
||||
complete -c $cmd -l rmextension -x -d "Remove a registered extension by name" -a "(__fabric_get_extensions)"
|
||||
complete -c $cmd -l strategy -x -d "Choose a strategy from the available strategies" -a "(__fabric_get_strategies)"
|
||||
complete -c $cmd -l voice -x -d "TTS voice name for supported models (e.g., Kore, Charon, Puck)" -a "(__fabric_get_gemini_voices)"
|
||||
complete -c $cmd -l transcribe-model -x -d "Model to use for transcription (separate from chat model)" -a "(__fabric_get_transcription_models)"
|
||||
|
||||
# Options that take a value from a fixed list
|
||||
complete -c $cmd -l thinking -x -d "Set reasoning/thinking level" -a "off low medium high"
|
||||
complete -c $cmd -l image-size -x -d "Image dimensions: 1024x1024, 1536x1024, 1024x1536, auto (default: auto)" -a "1024x1024 1536x1024 1024x1536 auto"
|
||||
complete -c $cmd -l image-quality -x -d "Image quality: low, medium, high, auto (default: auto)" -a "low medium high auto"
|
||||
complete -c $cmd -l image-background -x -d "Background type: opaque, transparent (default: opaque, only for PNG/WebP)" -a "opaque transparent"
|
||||
complete -c $cmd -l debug -x -d "Set debug level (0=off, 1=basic, 2=detailed, 3=trace, 4=wire)" -a "0 1 2 3 4"
|
||||
|
||||
# Options that take a file path
|
||||
complete -c $cmd -s a -l attachment -r -d "Attachment path or URL (e.g. for OpenAI image recognition messages)"
|
||||
complete -c $cmd -s o -l output -r -d "Output to file"
|
||||
complete -c $cmd -l config -r -d "Path to YAML config file" -a "(__fish_complete_suffix .yaml .yml)"
|
||||
complete -c $cmd -l addextension -r -d "Register a new extension from config file path" -a "(__fish_complete_suffix .yaml .yml)"
|
||||
complete -c $cmd -l image-file -r -d "Save generated image to specified file path (e.g., 'output.png')" -a "(__fish_complete_suffix .png .webp .jpeg .jpg)"
|
||||
complete -c $cmd -l transcribe-file -r -d "Audio or video file to transcribe" -a "(__fish_complete_suffix .mp3 .mp4 .mpeg .mpga .m4a .wav .webm)"
|
||||
|
||||
# Options that take a value the user types
|
||||
complete -c $cmd -s v -l variable -x -d "Values for pattern variables, e.g. -v=#role:expert -v=#points:30"
|
||||
complete -c $cmd -s t -l temperature -x -d "Set temperature (default: 0.7)"
|
||||
complete -c $cmd -s T -l topp -x -d "Set top P (default: 0.9)"
|
||||
complete -c $cmd -s P -l presencepenalty -x -d "Set presence penalty (default: 0.0)"
|
||||
complete -c $cmd -s F -l frequencypenalty -x -d "Set frequency penalty (default: 0.0)"
|
||||
complete -c $cmd -l modelContextLength -x -d "Model context length (only affects ollama)"
|
||||
complete -c $cmd -s n -l latest -x -d "Number of latest patterns to list (default: 0)"
|
||||
complete -c $cmd -s y -l youtube -x -d "YouTube video or play list URL to grab transcript, comments from it"
|
||||
complete -c $cmd -l visual-sensitivity -x -d "Tolerance for FFmpeg scene detection (0.0 - 1.0) (default: 0.4)"
|
||||
complete -c $cmd -l visual-fps -x -d "Extract a specific number of frames per second instead of using scene detection"
|
||||
complete -c $cmd -l yt-dlp-args -x -d "Additional arguments to pass to yt-dlp (e.g. '--cookies-from-browser brave')"
|
||||
complete -c $cmd -l spotify -x -d "Spotify podcast or episode URL to grab metadata from and send to chat"
|
||||
complete -c $cmd -s g -l language -x -d "Specify the Language Code for the chat, e.g. -g=en -g=zh"
|
||||
complete -c $cmd -s u -l scrape_url -x -d "Scrape website URL to markdown using Jina AI"
|
||||
complete -c $cmd -s q -l scrape_question -x -d "Search question using Jina AI"
|
||||
complete -c $cmd -s e -l seed -x -d "Seed to be used for LMM generation"
|
||||
complete -c $cmd -l address -x -d "The address to bind the REST API (default: :8080)"
|
||||
complete -c $cmd -l api-key -x -d "API key used to secure server routes"
|
||||
complete -c $cmd -l search-location -x -d "Set location for web search results (e.g., 'America/Los_Angeles')"
|
||||
complete -c $cmd -l image-compression -x -d "Compression level 0-100 for JPEG/WebP formats (default: not set)"
|
||||
complete -c $cmd -l think-start-tag -x -d "Start tag for thinking sections (default: <think>)"
|
||||
complete -c $cmd -l think-end-tag -x -d "End tag for thinking sections (default: </think>)"
|
||||
complete -c $cmd -l notification-command -x -d "Custom command to run for notifications (overrides built-in notifications)"
|
||||
|
||||
# Boolean flags (no arguments)
|
||||
complete -c $cmd -s S -l setup -d "Run setup for all reconfigurable parts of fabric"
|
||||
|
|
@ -123,12 +135,11 @@ function __fabric_register_completions
|
|||
complete -c $cmd -l visual -d "Extract visual data from video using OCR and FFmpeg"
|
||||
complete -c $cmd -l comments -d "Grab comments from YouTube video and send to chat"
|
||||
complete -c $cmd -l metadata -d "Output video metadata"
|
||||
complete -c $cmd -l yt-dlp-args -d "Additional arguments to pass to yt-dlp (e.g. '--cookies-from-browser brave')"
|
||||
complete -c $cmd -l readability -d "Convert HTML input into a clean, readable view"
|
||||
complete -c $cmd -l input-has-vars -d "Apply variables to user input"
|
||||
complete -c $cmd -l no-variable-replacement -d "Disable pattern variable replacement"
|
||||
complete -c $cmd -l dry-run -d "Show what would be sent to the model without actually sending it"
|
||||
complete -c $cmd -l search -d "Enable web search tool for supported models (Anthropic, OpenAI, Gemini)"
|
||||
complete -c $cmd -l search -d "Enable web search tool for supported models (Anthropic, OpenAI, Gemini, Grok)"
|
||||
complete -c $cmd -l serve -d "Serve the Fabric Rest API"
|
||||
complete -c $cmd -l serveOllama -d "Serve the Fabric Rest API with ollama endpoints"
|
||||
complete -c $cmd -l version -d "Print current version"
|
||||
|
|
@ -136,13 +147,14 @@ function __fabric_register_completions
|
|||
complete -c $cmd -l liststrategies -d "List all strategies"
|
||||
complete -c $cmd -l listvendors -d "List all vendors"
|
||||
complete -c $cmd -l list-gemini-voices -d "List all available Gemini TTS voices"
|
||||
complete -c $cmd -l list-transcription-models -d "List all available transcription models"
|
||||
complete -c $cmd -l shell-complete-list -d "Output raw list without headers/formatting (for shell completion)"
|
||||
complete -c $cmd -l suppress-think -d "Suppress text enclosed in thinking tags"
|
||||
complete -c $cmd -l disable-responses-api -d "Disable OpenAI Responses API (default: false)"
|
||||
complete -c $cmd -l split-media-file -d "Split audio/video files larger than 25MB using ffmpeg"
|
||||
complete -c $cmd -l notification -d "Send desktop notification when command completes"
|
||||
complete -c $cmd -l show-metadata -d "Print metadata (input/output tokens) to stderr"
|
||||
complete -c $cmd -s h -l help -d "Show this help message"
|
||||
complete -c $cmd -l spotify -d 'Spotify podcast or episode URL to grab metadata'
|
||||
end
|
||||
|
||||
__fabric_register_completions fabric
|
||||
|
|
|
|||
57
data/patterns/generate_frontmatter/system.md
Normal file
57
data/patterns/generate_frontmatter/system.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# IDENTITY and PURPOSE
|
||||
|
||||
You are an expert at knowledge management and note metadata. Given any text — a document, article, essay, book chapter, transcript, meeting notes, or rough draft — you generate clean, well-structured YAML frontmatter suitable for personal knowledge management (PKM) systems such as Obsidian, Logseq, or any markdown-based notes vault.
|
||||
|
||||
Your output is immediately paste-ready: valid YAML wrapped in `---` delimiters, placed at the top of the note.
|
||||
|
||||
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
|
||||
|
||||
# STEPS
|
||||
|
||||
- Read the entire input carefully to understand its content, type, and context.
|
||||
- Infer the most accurate and descriptive title if one is not explicitly present.
|
||||
- Identify the document type (article, chapter, meeting-notes, transcript, essay, reference, idea, etc.).
|
||||
- Extract 3–8 specific, lowercase tags that describe the content. Prefer concrete concepts over vague categories. Avoid single-word generic tags like "notes" or "text".
|
||||
- Generate 1–3 aliases: alternative titles or short names someone might search for.
|
||||
- Write a one-sentence summary (15–25 words) capturing the core argument or content.
|
||||
- Identify the author or source if present; leave blank if not.
|
||||
- Use today's date or the document date if detectable; otherwise omit the date field.
|
||||
- Note the document's primary domain or area (e.g., productivity, philosophy, technology, science, history).
|
||||
|
||||
# OUTPUT
|
||||
|
||||
Output ONLY the YAML frontmatter block. No explanation, no preamble, no commentary after the block.
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Exact or inferred title"
|
||||
aliases:
|
||||
- "Short name"
|
||||
- "Alternative title"
|
||||
tags:
|
||||
- specific-tag
|
||||
- another-tag
|
||||
- domain/subtopic
|
||||
type: article # article | chapter | transcript | meeting-notes | essay | reference | idea | book
|
||||
author: "Author Name" # omit if unknown
|
||||
source: "" # URL or citation if available; omit if not
|
||||
date: YYYY-MM-DD # omit if not determinable
|
||||
summary: "One sentence capturing the core content or argument of this document."
|
||||
status: unprocessed # unprocessed | reading | processed | archived
|
||||
---
|
||||
```
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- Output ONLY the YAML block — nothing before `---` and nothing after the closing `---`.
|
||||
- Use lowercase for all tags. Use hyphens for multi-word tags (e.g., `knowledge-management`, not `KnowledgeManagement`).
|
||||
- For hierarchical tags use slash notation: `philosophy/stoicism`, `technology/ai`.
|
||||
- Be specific: `decision-making` is better than `thinking`; `ancient-rome` is better than `history`.
|
||||
- The summary must be a complete sentence, not a fragment.
|
||||
- Omit fields that cannot be reasonably inferred (author, source, date) rather than guessing.
|
||||
- Do not add any fields not shown in the template above.
|
||||
- Do not give warnings or notes; only output the YAML block.
|
||||
|
||||
# INPUT
|
||||
|
||||
INPUT:
|
||||
|
|
@ -175,84 +175,85 @@
|
|||
171. **find_logical_fallacies**: Identifies and analyzes fallacies in arguments, classifying them as formal or informal with detailed reasoning.
|
||||
172. **fix_typos**: Proofreads and corrects typos, spelling, grammar, and punctuation errors in text.
|
||||
173. **generate_code_rules**: Compile best-practice coding rules and guardrails for AI-assisted development workflows from the provided content.
|
||||
174. **get_wow_per_minute**: Determines the wow-factor of content per minute based on surprise, novelty, insight, value, and wisdom, measuring how rewarding the content is for the viewer.
|
||||
175. **greybeard_secure_prompt_engineer**: Creates secure, production-grade system prompts with NASA-style mission assurance, outputting hardened prompts, injection test suites, and evaluation rubrics.
|
||||
176. **heal_person**: Develops a comprehensive plan for spiritual and mental healing based on psychological profiles, providing personalized recommendations for mental health improvement and overall life enhancement.
|
||||
177. **humanize**: Rewrites AI-generated text to sound natural, conversational, and easy to understand, maintaining clarity and simplicity.
|
||||
178. **identify_dsrp_distinctions**: Encourages creative, systems-based thinking by exploring distinctions, boundaries, and their implications, drawing on insights from prominent systems thinkers.
|
||||
179. **identify_dsrp_perspectives**: Explores the concept of distinctions in systems thinking, focusing on how boundaries define ideas, influence understanding, and reveal or obscure insights.
|
||||
180. **identify_dsrp_relationships**: Encourages exploration of connections, distinctions, and boundaries between ideas, inspired by systems thinkers to reveal new insights and patterns in complex systems.
|
||||
181. **identify_dsrp_systems**: Encourages organizing ideas into systems of parts and wholes, inspired by systems thinkers to explore relationships and how changes in organization impact meaning and understanding.
|
||||
182. **identify_job_stories**: Identifies key job stories or requirements for roles.
|
||||
183. **improve_academic_writing**: Refines text into clear, concise academic language while improving grammar, coherence, and clarity, with a list of changes.
|
||||
184. **improve_prompt**: Improves an LLM/AI prompt by applying expert prompt writing strategies for better results and clarity.
|
||||
185. **improve_report_finding**: Improves a penetration test security finding by providing detailed descriptions, risks, recommendations, references, quotes, and a concise summary in markdown format.
|
||||
186. **improve_writing**: Refines text by correcting grammar, enhancing style, improving clarity, and maintaining the original meaning. skills.
|
||||
187. **judge_output**: Evaluates Honeycomb queries by judging their effectiveness, providing critiques and outcomes based on language nuances and analytics relevance.
|
||||
188. **label_and_rate**: Labels content with up to 20 single-word tags and rates it based on idea count and relevance to human meaning, AI, and other related themes, assigning a tier (S, A, B, C, D) and a quality score.
|
||||
189. **md_callout**: Classifies content and generates a markdown callout based on the provided text, selecting the most appropriate type.
|
||||
190. **model_as_sherlock_freud**: Builds psychological models using detective reasoning and psychoanalytic insight to understand human behavior.
|
||||
191. **official_pattern_template**: Template to use if you want to create new fabric patterns.
|
||||
192. **predict_person_actions**: Predicts behavioral responses based on psychological profiles and challenges.
|
||||
193. **prepare_7s_strategy**: Prepares a comprehensive briefing document from 7S's strategy capturing organizational profile, strategic elements, and market dynamics with clear, concise, and organized content.
|
||||
194. **provide_guidance**: Provides psychological and life coaching advice, including analysis, recommendations, and potential diagnoses, with a compassionate and honest tone.
|
||||
195. **rate_ai_response**: Rates the quality of AI responses by comparing them to top human expert performance, assigning a letter grade, reasoning, and providing a 1-100 score based on the evaluation.
|
||||
196. **rate_ai_result**: Assesses the quality of AI/ML/LLM work by deeply analyzing content, instructions, and output, then rates performance based on multiple dimensions, including coverage, creativity, and interdisciplinary thinking.
|
||||
197. **rate_content**: Labels content with up to 20 single-word tags and rates it based on idea count and relevance to human meaning, AI, and other related themes, assigning a tier (S, A, B, C, D) and a quality score.
|
||||
198. **rate_value**: Produces the best possible output by deeply analyzing and understanding the input and its intended purpose.
|
||||
199. **raw_query**: Fully digests and contemplates the input to produce the best possible result based on understanding the sender's intent.
|
||||
200. **recommend_artists**: Recommends a personalized festival schedule with artists aligned to your favorite styles and interests, including rationale.
|
||||
201. **recommend_pipeline_upgrades**: Optimizes vulnerability-checking pipelines by incorporating new information and improving their efficiency, with detailed explanations of changes.
|
||||
202. **recommend_talkpanel_topics**: Produces a clean set of proposed talks or panel talking points for a person based on their interests and goals, formatted for submission to a conference organizer.
|
||||
203. **recommend_yoga_practice**: Provides personalized yoga sequences, meditation guidance, and holistic lifestyle advice based on individual profiles.
|
||||
204. **refine_design_document**: Refines a design document based on a design review by analyzing, mapping concepts, and implementing changes using valid Markdown.
|
||||
205. **review_design**: Reviews and analyzes architecture design, focusing on clarity, component design, system integrations, security, performance, scalability, and data management.
|
||||
206. **review_code**: Performs a comprehensive code review, providing detailed feedback on correctness, security, and performance.
|
||||
207. **sanitize_broken_html_to_markdown**: Converts messy HTML into clean, properly formatted Markdown, applying custom styling and ensuring compatibility with Vite.
|
||||
208. **suggest_pattern**: Suggests appropriate fabric patterns or commands based on user input, providing clear explanations and options for users.
|
||||
209. **suggest_gt_command**: Suggest optimal Gas Town (GT) commands based on user intent and task description.
|
||||
210. **suggest_openclaw_pattern**: Suggests the most appropriate Openclaw CLI command based on user intent, mapping natural language requests to commands for messaging, device management, scheduling, and automation.
|
||||
211. **summarize**: Summarizes content into a 20-word sentence, main points, and takeaways, formatted with numbered lists in Markdown.
|
||||
212. **summarize_board_meeting**: Creates formal meeting notes from board meeting transcripts for corporate governance documentation.
|
||||
213. **summarize_debate**: Summarizes debates, identifies primary disagreement, extracts arguments, and provides analysis of evidence and argument strength to predict outcomes.
|
||||
214. **summarize_git_changes**: Summarizes recent project updates from the last 7 days, focusing on key changes with enthusiasm.
|
||||
215. **summarize_git_diff**: Summarizes and organizes Git diff changes with clear, succinct commit messages and bullet points.
|
||||
216. **summarize_lecture**: Extracts relevant topics, definitions, and tools from lecture transcripts, providing structured summaries with timestamps and key takeaways.
|
||||
217. **summarize_legislation**: Summarizes complex political proposals and legislation by analyzing key points, proposed changes, and providing balanced, positive, and cynical characterizations.
|
||||
218. **summarize_meeting**: Analyzes meeting transcripts to extract a structured summary, including an overview, key points, tasks, decisions, challenges, timeline, references, and next steps.
|
||||
219. **summarize_micro**: Summarizes content into a 20-word sentence, 3 main points, and 3 takeaways, formatted in clear, concise Markdown.
|
||||
220. **summarize_newsletter**: Extracts the most meaningful, interesting, and useful content from a newsletter, summarizing key sections such as content, opinions, tools, companies, and follow-up items in clear, structured Markdown.
|
||||
221. **summarize_paper**: Summarizes an academic paper by detailing its title, authors, technical approach, distinctive features, experimental setup, results, advantages, limitations, and conclusion in a clear, structured format using human-readable Markdown.
|
||||
222. **summarize_prompt**: Summarizes AI chat prompts by describing the primary function, unique approach, and expected output in a concise paragraph. The summary is focused on the prompt's purpose without unnecessary details or formatting.
|
||||
223. **summarize_pull-requests**: Summarizes pull requests for a coding project by providing a summary and listing the top PRs with human-readable descriptions.
|
||||
224. **summarize_rpg_session**: Summarizes a role-playing game session by extracting key events, combat stats, character changes, quotes, and more.
|
||||
225. **t_analyze_challenge_handling**: Provides 8-16 word bullet points evaluating how well challenges are being addressed, calling out any lack of effort.
|
||||
226. **t_check_dunning_kruger**: Assess narratives for Dunning-Kruger patterns by contrasting self-perception with demonstrated competence and confidence cues.
|
||||
227. **t_check_metrics**: Analyzes deep context from the TELOS file and input instruction, then provides a wisdom-based output while considering metrics and KPIs to assess recent improvements.
|
||||
228. **t_create_h3_career**: Summarizes context and produces wisdom-based output by deeply analyzing both the TELOS File and the input instruction, considering the relationship between the two.
|
||||
229. **t_create_opening_sentences**: Describes from TELOS file the person's identity, goals, and actions in 4 concise, 32-word bullet points, humbly.
|
||||
230. **t_describe_life_outlook**: Describes from TELOS file a person's life outlook in 5 concise, 16-word bullet points.
|
||||
231. **t_extract_intro_sentences**: Summarizes from TELOS file a person's identity, work, and current projects in 5 concise and grounded bullet points.
|
||||
232. **t_extract_panel_topics**: Creates 5 panel ideas with titles and descriptions based on deep context from a TELOS file and input.
|
||||
233. **t_find_blindspots**: Identify potential blindspots in thinking, frames, or models that may expose the individual to error or risk.
|
||||
234. **t_find_negative_thinking**: Analyze a TELOS file and input to identify negative thinking in documents or journals, followed by tough love encouragement.
|
||||
235. **t_find_neglected_goals**: Analyze a TELOS file and input instructions to identify goals or projects that have not been worked on recently.
|
||||
236. **t_give_encouragement**: Analyze a TELOS file and input instructions to evaluate progress, provide encouragement, and offer recommendations for continued effort.
|
||||
237. **t_red_team_thinking**: Analyze a TELOS file and input instructions to red-team thinking, models, and frames, then provide recommendations for improvement.
|
||||
238. **t_threat_model_plans**: Analyze a TELOS file and input instructions to create threat models for a life plan and recommend improvements.
|
||||
239. **t_visualize_mission_goals_projects**: Analyze a TELOS file and input instructions to create an ASCII art diagram illustrating the relationship of missions, goals, and projects.
|
||||
240. **t_year_in_review**: Analyze a TELOS file to create insights about a person or entity, then summarize accomplishments and visualizations in bullet points.
|
||||
241. **to_flashcards**: Create Anki flashcards from a given text, focusing on concise, optimized questions and answers without external context.
|
||||
242. **transcribe_minutes**: Extracts (from meeting transcription) meeting minutes, identifying actionables, insightful ideas, decisions, challenges, and next steps in a structured format.
|
||||
243. **translate**: Translates sentences or documentation into the specified language code while maintaining the original formatting and tone.
|
||||
244. **tweet**: Provides a step-by-step guide on crafting engaging tweets with emojis, covering Twitter basics, account creation, features, and audience targeting.
|
||||
245. **ultimate_law_safety**: Evaluates actions, policies, or systems against the Ultimate Law framework — a minimal, falsifiable ethical constraint that prohibits creating unwilling victims.
|
||||
246. **write_essay**: Writes essays in the style of a specified author, embodying their unique voice, vocabulary, and approach. Uses `author_name` variable.
|
||||
247. **write_essay_pg**: Writes concise, clear essays in the style of Paul Graham, focusing on simplicity, clarity, and illumination of the provided topic.
|
||||
248. **write_hackerone_report**: Generates concise, clear, and reproducible bug bounty reports, detailing vulnerability impact, steps to reproduce, and exploit details for triagers.
|
||||
249. **write_latex**: Generates syntactically correct LaTeX code for a new.tex document, ensuring proper formatting and compatibility with pdflatex.
|
||||
250. **write_micro_essay**: Writes concise, clear, and illuminating essays on the given topic in the style of Paul Graham.
|
||||
251. **write_nuclei_template_rule**: Generates Nuclei YAML templates for detecting vulnerabilities using HTTP requests, matchers, extractors, and dynamic data extraction.
|
||||
252. **write_pull-request**: Drafts detailed pull request descriptions, explaining changes, providing reasoning, and identifying potential bugs from the git diff command output.
|
||||
253. **write_semgrep_rule**: Creates accurate and working Semgrep rules based on input, following syntax guidelines and specific language considerations.
|
||||
254. **youtube_summary**: Create concise, timestamped Youtube video summaries that highlight key points.
|
||||
174. **generate_frontmatter**: Generates paste-ready YAML frontmatter for PKM notes, with title, aliases, tags, type, summary, and status fields.
|
||||
175. **get_wow_per_minute**: Determines the wow-factor of content per minute based on surprise, novelty, insight, value, and wisdom, measuring how rewarding the content is for the viewer.
|
||||
176. **greybeard_secure_prompt_engineer**: Creates secure, production-grade system prompts with NASA-style mission assurance, outputting hardened prompts, injection test suites, and evaluation rubrics.
|
||||
177. **heal_person**: Develops a comprehensive plan for spiritual and mental healing based on psychological profiles, providing personalized recommendations for mental health improvement and overall life enhancement.
|
||||
178. **humanize**: Rewrites AI-generated text to sound natural, conversational, and easy to understand, maintaining clarity and simplicity.
|
||||
179. **identify_dsrp_distinctions**: Encourages creative, systems-based thinking by exploring distinctions, boundaries, and their implications, drawing on insights from prominent systems thinkers.
|
||||
180. **identify_dsrp_perspectives**: Explores the concept of distinctions in systems thinking, focusing on how boundaries define ideas, influence understanding, and reveal or obscure insights.
|
||||
181. **identify_dsrp_relationships**: Encourages exploration of connections, distinctions, and boundaries between ideas, inspired by systems thinkers to reveal new insights and patterns in complex systems.
|
||||
182. **identify_dsrp_systems**: Encourages organizing ideas into systems of parts and wholes, inspired by systems thinkers to explore relationships and how changes in organization impact meaning and understanding.
|
||||
183. **identify_job_stories**: Identifies key job stories or requirements for roles.
|
||||
184. **improve_academic_writing**: Refines text into clear, concise academic language while improving grammar, coherence, and clarity, with a list of changes.
|
||||
185. **improve_prompt**: Improves an LLM/AI prompt by applying expert prompt writing strategies for better results and clarity.
|
||||
186. **improve_report_finding**: Improves a penetration test security finding by providing detailed descriptions, risks, recommendations, references, quotes, and a concise summary in markdown format.
|
||||
187. **improve_writing**: Refines text by correcting grammar, enhancing style, improving clarity, and maintaining the original meaning. skills.
|
||||
188. **judge_output**: Evaluates Honeycomb queries by judging their effectiveness, providing critiques and outcomes based on language nuances and analytics relevance.
|
||||
189. **label_and_rate**: Labels content with up to 20 single-word tags and rates it based on idea count and relevance to human meaning, AI, and other related themes, assigning a tier (S, A, B, C, D) and a quality score.
|
||||
190. **md_callout**: Classifies content and generates a markdown callout based on the provided text, selecting the most appropriate type.
|
||||
191. **model_as_sherlock_freud**: Builds psychological models using detective reasoning and psychoanalytic insight to understand human behavior.
|
||||
192. **official_pattern_template**: Template to use if you want to create new fabric patterns.
|
||||
193. **predict_person_actions**: Predicts behavioral responses based on psychological profiles and challenges.
|
||||
194. **prepare_7s_strategy**: Prepares a comprehensive briefing document from 7S's strategy capturing organizational profile, strategic elements, and market dynamics with clear, concise, and organized content.
|
||||
195. **provide_guidance**: Provides psychological and life coaching advice, including analysis, recommendations, and potential diagnoses, with a compassionate and honest tone.
|
||||
196. **rate_ai_response**: Rates the quality of AI responses by comparing them to top human expert performance, assigning a letter grade, reasoning, and providing a 1-100 score based on the evaluation.
|
||||
197. **rate_ai_result**: Assesses the quality of AI/ML/LLM work by deeply analyzing content, instructions, and output, then rates performance based on multiple dimensions, including coverage, creativity, and interdisciplinary thinking.
|
||||
198. **rate_content**: Labels content with up to 20 single-word tags and rates it based on idea count and relevance to human meaning, AI, and other related themes, assigning a tier (S, A, B, C, D) and a quality score.
|
||||
199. **rate_value**: Produces the best possible output by deeply analyzing and understanding the input and its intended purpose.
|
||||
200. **raw_query**: Fully digests and contemplates the input to produce the best possible result based on understanding the sender's intent.
|
||||
201. **recommend_artists**: Recommends a personalized festival schedule with artists aligned to your favorite styles and interests, including rationale.
|
||||
202. **recommend_pipeline_upgrades**: Optimizes vulnerability-checking pipelines by incorporating new information and improving their efficiency, with detailed explanations of changes.
|
||||
203. **recommend_talkpanel_topics**: Produces a clean set of proposed talks or panel talking points for a person based on their interests and goals, formatted for submission to a conference organizer.
|
||||
204. **recommend_yoga_practice**: Provides personalized yoga sequences, meditation guidance, and holistic lifestyle advice based on individual profiles.
|
||||
205. **refine_design_document**: Refines a design document based on a design review by analyzing, mapping concepts, and implementing changes using valid Markdown.
|
||||
206. **review_design**: Reviews and analyzes architecture design, focusing on clarity, component design, system integrations, security, performance, scalability, and data management.
|
||||
207. **review_code**: Performs a comprehensive code review, providing detailed feedback on correctness, security, and performance.
|
||||
208. **sanitize_broken_html_to_markdown**: Converts messy HTML into clean, properly formatted Markdown, applying custom styling and ensuring compatibility with Vite.
|
||||
209. **suggest_pattern**: Suggests appropriate fabric patterns or commands based on user input, providing clear explanations and options for users.
|
||||
210. **suggest_gt_command**: Suggest optimal Gas Town (GT) commands based on user intent and task description.
|
||||
211. **suggest_openclaw_pattern**: Suggests the most appropriate Openclaw CLI command based on user intent, mapping natural language requests to commands for messaging, device management, scheduling, and automation.
|
||||
212. **summarize**: Summarizes content into a 20-word sentence, main points, and takeaways, formatted with numbered lists in Markdown.
|
||||
213. **summarize_board_meeting**: Creates formal meeting notes from board meeting transcripts for corporate governance documentation.
|
||||
214. **summarize_debate**: Summarizes debates, identifies primary disagreement, extracts arguments, and provides analysis of evidence and argument strength to predict outcomes.
|
||||
215. **summarize_git_changes**: Summarizes recent project updates from the last 7 days, focusing on key changes with enthusiasm.
|
||||
216. **summarize_git_diff**: Summarizes and organizes Git diff changes with clear, succinct commit messages and bullet points.
|
||||
217. **summarize_lecture**: Extracts relevant topics, definitions, and tools from lecture transcripts, providing structured summaries with timestamps and key takeaways.
|
||||
218. **summarize_legislation**: Summarizes complex political proposals and legislation by analyzing key points, proposed changes, and providing balanced, positive, and cynical characterizations.
|
||||
219. **summarize_meeting**: Analyzes meeting transcripts to extract a structured summary, including an overview, key points, tasks, decisions, challenges, timeline, references, and next steps.
|
||||
220. **summarize_micro**: Summarizes content into a 20-word sentence, 3 main points, and 3 takeaways, formatted in clear, concise Markdown.
|
||||
221. **summarize_newsletter**: Extracts the most meaningful, interesting, and useful content from a newsletter, summarizing key sections such as content, opinions, tools, companies, and follow-up items in clear, structured Markdown.
|
||||
222. **summarize_paper**: Summarizes an academic paper by detailing its title, authors, technical approach, distinctive features, experimental setup, results, advantages, limitations, and conclusion in a clear, structured format using human-readable Markdown.
|
||||
223. **summarize_prompt**: Summarizes AI chat prompts by describing the primary function, unique approach, and expected output in a concise paragraph. The summary is focused on the prompt's purpose without unnecessary details or formatting.
|
||||
224. **summarize_pull-requests**: Summarizes pull requests for a coding project by providing a summary and listing the top PRs with human-readable descriptions.
|
||||
225. **summarize_rpg_session**: Summarizes a role-playing game session by extracting key events, combat stats, character changes, quotes, and more.
|
||||
226. **t_analyze_challenge_handling**: Provides 8-16 word bullet points evaluating how well challenges are being addressed, calling out any lack of effort.
|
||||
227. **t_check_dunning_kruger**: Assess narratives for Dunning-Kruger patterns by contrasting self-perception with demonstrated competence and confidence cues.
|
||||
228. **t_check_metrics**: Analyzes deep context from the TELOS file and input instruction, then provides a wisdom-based output while considering metrics and KPIs to assess recent improvements.
|
||||
229. **t_create_h3_career**: Summarizes context and produces wisdom-based output by deeply analyzing both the TELOS File and the input instruction, considering the relationship between the two.
|
||||
230. **t_create_opening_sentences**: Describes from TELOS file the person's identity, goals, and actions in 4 concise, 32-word bullet points, humbly.
|
||||
231. **t_describe_life_outlook**: Describes from TELOS file a person's life outlook in 5 concise, 16-word bullet points.
|
||||
232. **t_extract_intro_sentences**: Summarizes from TELOS file a person's identity, work, and current projects in 5 concise and grounded bullet points.
|
||||
233. **t_extract_panel_topics**: Creates 5 panel ideas with titles and descriptions based on deep context from a TELOS file and input.
|
||||
234. **t_find_blindspots**: Identify potential blindspots in thinking, frames, or models that may expose the individual to error or risk.
|
||||
235. **t_find_negative_thinking**: Analyze a TELOS file and input to identify negative thinking in documents or journals, followed by tough love encouragement.
|
||||
236. **t_find_neglected_goals**: Analyze a TELOS file and input instructions to identify goals or projects that have not been worked on recently.
|
||||
237. **t_give_encouragement**: Analyze a TELOS file and input instructions to evaluate progress, provide encouragement, and offer recommendations for continued effort.
|
||||
238. **t_red_team_thinking**: Analyze a TELOS file and input instructions to red-team thinking, models, and frames, then provide recommendations for improvement.
|
||||
239. **t_threat_model_plans**: Analyze a TELOS file and input instructions to create threat models for a life plan and recommend improvements.
|
||||
240. **t_visualize_mission_goals_projects**: Analyze a TELOS file and input instructions to create an ASCII art diagram illustrating the relationship of missions, goals, and projects.
|
||||
241. **t_year_in_review**: Analyze a TELOS file to create insights about a person or entity, then summarize accomplishments and visualizations in bullet points.
|
||||
242. **to_flashcards**: Create Anki flashcards from a given text, focusing on concise, optimized questions and answers without external context.
|
||||
243. **transcribe_minutes**: Extracts (from meeting transcription) meeting minutes, identifying actionables, insightful ideas, decisions, challenges, and next steps in a structured format.
|
||||
244. **translate**: Translates sentences or documentation into the specified language code while maintaining the original formatting and tone.
|
||||
245. **tweet**: Provides a step-by-step guide on crafting engaging tweets with emojis, covering Twitter basics, account creation, features, and audience targeting.
|
||||
246. **ultimate_law_safety**: Evaluates actions, policies, or systems against the Ultimate Law framework — a minimal, falsifiable ethical constraint that prohibits creating unwilling victims.
|
||||
247. **write_essay**: Writes essays in the style of a specified author, embodying their unique voice, vocabulary, and approach. Uses `author_name` variable.
|
||||
248. **write_essay_pg**: Writes concise, clear essays in the style of Paul Graham, focusing on simplicity, clarity, and illumination of the provided topic.
|
||||
249. **write_hackerone_report**: Generates concise, clear, and reproducible bug bounty reports, detailing vulnerability impact, steps to reproduce, and exploit details for triagers.
|
||||
250. **write_latex**: Generates syntactically correct LaTeX code for a new.tex document, ensuring proper formatting and compatibility with pdflatex.
|
||||
251. **write_micro_essay**: Writes concise, clear, and illuminating essays on the given topic in the style of Paul Graham.
|
||||
252. **write_nuclei_template_rule**: Generates Nuclei YAML templates for detecting vulnerabilities using HTTP requests, matchers, extractors, and dynamic data extraction.
|
||||
253. **write_pull-request**: Drafts detailed pull request descriptions, explaining changes, providing reasoning, and identifying potential bugs from the git diff command output.
|
||||
254. **write_semgrep_rule**: Creates accurate and working Semgrep rules based on input, following syntax guidelines and specific language considerations.
|
||||
255. **youtube_summary**: Create concise, timestamped Youtube video summaries that highlight key points.
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ Match the request to one or more of these primary categories:
|
|||
|
||||
**CLASSIFICATION**: apply_ul_tags
|
||||
|
||||
**CONVERSION**: clean_text, convert_to_markdown, create_graph_from_input, create_slides, export_data_as_csv, extract_videoid, humanize, md_callout, sanitize_broken_html_to_markdown, to_flashcards, transcribe_minutes, translate, tweet, write_latex
|
||||
**CONVERSION**: clean_text, convert_to_markdown, create_graph_from_input, create_slides, export_data_as_csv, extract_videoid, generate_frontmatter, humanize, md_callout, sanitize_broken_html_to_markdown, to_flashcards, transcribe_minutes, translate, tweet, write_latex
|
||||
|
||||
**CR THINKING**: audit_consent, audit_transparency, capture_thinkers_work, check_falsifiability, create_idea_compass, create_markmap_visualization, create_upgrade_pack, detect_mind_virus, detect_silent_victims, dialog_with_socrates, extract_alpha, extract_controversial_ideas, extract_ethical_framework, extract_extraordinary_claims, extract_predictions, extract_primary_problem, extract_wisdom_nometa, find_hidden_message, find_logical_fallacies, summarize_debate, t_analyze_challenge_handling, t_check_dunning_kruger, t_find_blindspots, t_find_negative_thinking, t_find_neglected_goals, t_red_team_thinking, ultimate_law_safety
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ Match the request to one or more of these primary categories:
|
|||
|
||||
**DEVOPS**: analyze_terraform_plan
|
||||
|
||||
**EXTRACT**: analyze_comments, create_aphorisms, create_golden_rules, create_tags, create_upgrade_pack, create_video_chapters, extract_affiliate_products, extract_algorithm_update_recommendations, extract_all_quotes, extract_alpha, extract_article_wisdom, extract_bd_ideas, extract_book_ideas, extract_book_recommendations, extract_business_ideas, extract_characters, extract_controversial_ideas, extract_core_message, extract_ctf_writeup, extract_domains, extract_ethical_framework, extract_extraordinary_claims, extract_ideas, extract_insights, extract_insights_dm, extract_instructions, extract_jokes, extract_latest_video, extract_main_activities, extract_main_idea, extract_mcp_servers, extract_most_redeeming_thing, extract_patterns, extract_poc, extract_predictions, extract_primary_problem, extract_primary_solution, extract_product_features, extract_questions, extract_recipe, extract_recommendations, extract_references, extract_skills, extract_song_meaning, extract_sponsors, extract_video_commerce_entities, extract_videoid, extract_wisdom, extract_wisdom_agents, extract_wisdom_dm, extract_wisdom_nometa, extract_wisdom_short, extract_wisdom_with_attribution, generate_code_rules, t_extract_intro_sentences, t_extract_panel_topics
|
||||
**EXTRACT**: analyze_comments, create_aphorisms, create_golden_rules, create_tags, create_upgrade_pack, create_video_chapters, extract_affiliate_products, extract_algorithm_update_recommendations, extract_all_quotes, extract_alpha, extract_article_wisdom, extract_bd_ideas, extract_book_ideas, extract_book_recommendations, extract_business_ideas, extract_characters, extract_controversial_ideas, extract_core_message, extract_ctf_writeup, extract_domains, extract_ethical_framework, extract_extraordinary_claims, extract_ideas, extract_insights, extract_insights_dm, extract_instructions, extract_jokes, extract_latest_video, extract_main_activities, extract_main_idea, extract_mcp_servers, extract_most_redeeming_thing, extract_patterns, extract_poc, extract_predictions, extract_primary_problem, extract_primary_solution, extract_product_features, extract_questions, extract_recipe, extract_recommendations, extract_references, extract_skills, extract_song_meaning, extract_sponsors, extract_video_commerce_entities, extract_videoid, extract_wisdom, extract_wisdom_agents, extract_wisdom_dm, extract_wisdom_nometa, extract_wisdom_short, extract_wisdom_with_attribution, generate_code_rules, generate_frontmatter, t_extract_intro_sentences, t_extract_panel_topics
|
||||
|
||||
**GAMING**: create_npc, create_rpg_summary, summarize_rpg_session
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ Match the request to one or more of these primary categories:
|
|||
|
||||
**WELLNESS**: analyze_spiritual_text, create_better_frame, extract_wisdom_dm, heal_person, model_as_sherlock_freud, predict_person_actions, provide_guidance, recommend_yoga_practice, t_give_encouragement
|
||||
|
||||
**WRITING**: analyze_prose_json, analyze_prose_pinker, apply_ul_tags, clean_text, compare_and_contrast, convert_to_markdown, create_5_sentence_summary, create_academic_paper, create_aphorisms, create_better_frame, create_design_document, create_design_system, create_diy, create_formal_email, create_hormozi_offer, create_keynote, create_micro_summary, create_newsletter_entry, create_prediction_block, create_prd, create_show_intro, create_slides, create_story_about_people_interaction, create_story_explanation, create_summary, create_tags, create_user_story, enrich_blog_post, explain_docs, explain_terms, fix_typos, humanize, improve_academic_writing, improve_writing, label_and_rate, md_callout, official_pattern_template, recommend_talkpanel_topics, refine_design_document, summarize, summarize_debate, summarize_lecture, summarize_legislation, summarize_meeting, summarize_micro, summarize_newsletter, summarize_paper, summarize_rpg_session, t_create_opening_sentences, t_describe_life_outlook, t_extract_intro_sentences, t_extract_panel_topics, t_give_encouragement, t_year_in_review, transcribe_minutes, tweet, write_essay, write_essay_pg, write_hackerone_report, write_latex, write_micro_essay, write_pull-request
|
||||
**WRITING**: analyze_prose_json, analyze_prose_pinker, apply_ul_tags, clean_text, compare_and_contrast, convert_to_markdown, create_5_sentence_summary, create_academic_paper, create_aphorisms, create_better_frame, create_design_document, create_design_system, create_diy, create_formal_email, create_hormozi_offer, create_keynote, create_micro_summary, create_newsletter_entry, create_prediction_block, create_prd, create_show_intro, create_slides, create_story_about_people_interaction, create_story_explanation, create_summary, create_tags, create_user_story, enrich_blog_post, explain_docs, explain_terms, fix_typos, generate_frontmatter, humanize, improve_academic_writing, improve_writing, label_and_rate, md_callout, official_pattern_template, recommend_talkpanel_topics, refine_design_document, summarize, summarize_debate, summarize_lecture, summarize_legislation, summarize_meeting, summarize_micro, summarize_newsletter, summarize_paper, summarize_rpg_session, t_create_opening_sentences, t_describe_life_outlook, t_extract_intro_sentences, t_extract_panel_topics, t_give_encouragement, t_year_in_review, transcribe_minutes, tweet, write_essay, write_essay_pg, write_hackerone_report, write_latex, write_micro_essay, write_pull-request
|
||||
|
||||
## Workflow Suggestions
|
||||
|
||||
|
|
|
|||
|
|
@ -702,6 +702,10 @@ Create glossaries of advanced terms with definitions and analogies.
|
|||
|
||||
Proofreads and corrects typos, spelling, grammar, and punctuation errors.
|
||||
|
||||
### generate_frontmatter
|
||||
|
||||
Generate YAML frontmatter with tags, aliases and summary for PKM notes.
|
||||
|
||||
### humanize
|
||||
|
||||
Transform technical content into approachable language.
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ Use parameterized queries instead of string concatenation...
|
|||
- Store secrets in environment variables or secure configuration
|
||||
- Use the built-in setup process for key management
|
||||
- Regularly rotate API keys
|
||||
- `~/.config/fabric/.env` holds API keys and Codex OAuth tokens. Fabric writes it with mode `0600`. Treat the file as secret material. Comments and key order are not preserved when Codex tokens are updated in place.
|
||||
|
||||
### Input Validation
|
||||
|
||||
|
|
|
|||
|
|
@ -112,14 +112,18 @@ For manual installation or troubleshooting, see the detailed instructions below.
|
|||
|
||||
The completions provide intelligent suggestions for:
|
||||
|
||||
- **Patterns**: Tab-complete available patterns with `-p` or `--pattern`
|
||||
- **Patterns**: Tab-complete available patterns with `-p`, `--pattern`, or `--readpattern`
|
||||
- **Models**: Tab-complete available models with `-m` or `--model`
|
||||
- **Vendors**: Tab-complete configured vendors with `-V` or `--vendor`
|
||||
- **Contexts**: Tab-complete contexts for context-related flags
|
||||
- **Sessions**: Tab-complete sessions for session-related flags
|
||||
- **Strategies**: Tab-complete available strategies
|
||||
- **Extensions**: Tab-complete registered extensions
|
||||
- **Gemini Voices**: Tab-complete TTS voices for `--voice`
|
||||
- **File paths**: Smart file completion for attachment, output, and config options
|
||||
- **Transcription models**: Tab-complete models for `--transcribe-model`
|
||||
- **Fixed value sets**: Tab-complete the accepted values of `--thinking`, `--debug`, `--image-size`,
|
||||
`--image-quality`, and `--image-background`
|
||||
- **File paths**: Smart file completion for attachment, output, config, image, and transcription options
|
||||
- **Flag completion**: All available command-line flags and options
|
||||
|
||||
## Alternative Installation Method
|
||||
|
|
|
|||
13
docs/docs.go
13
docs/docs.go
|
|
@ -139,6 +139,15 @@ const docTemplate = `{
|
|||
"$ref": "#/definitions/fsdb.Pattern"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
|
|
@ -346,7 +355,6 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
},
|
||||
"language": {
|
||||
"description": "Add Language field to bind from request",
|
||||
"type": "string"
|
||||
},
|
||||
"maxTokens": {
|
||||
|
|
@ -412,9 +420,6 @@ const docTemplate = `{
|
|||
"type": "number",
|
||||
"format": "float64"
|
||||
},
|
||||
"updateChan": {
|
||||
"type": "object"
|
||||
},
|
||||
"voice": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,15 @@
|
|||
"$ref": "#/definitions/fsdb.Pattern"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
|
|
@ -340,7 +349,6 @@
|
|||
"type": "string"
|
||||
},
|
||||
"language": {
|
||||
"description": "Add Language field to bind from request",
|
||||
"type": "string"
|
||||
},
|
||||
"maxTokens": {
|
||||
|
|
@ -406,9 +414,6 @@
|
|||
"type": "number",
|
||||
"format": "float64"
|
||||
},
|
||||
"updateChan": {
|
||||
"type": "object"
|
||||
},
|
||||
"voice": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ definitions:
|
|||
imageSize:
|
||||
type: string
|
||||
language:
|
||||
description: Add Language field to bind from request
|
||||
type: string
|
||||
maxTokens:
|
||||
type: integer
|
||||
|
|
@ -95,8 +94,6 @@ definitions:
|
|||
topP:
|
||||
format: float64
|
||||
type: number
|
||||
updateChan:
|
||||
type: object
|
||||
voice:
|
||||
type: string
|
||||
type: object
|
||||
|
|
@ -265,6 +262,12 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/fsdb.Pattern'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1765472234,
|
||||
"narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=",
|
||||
"lastModified": 1785692966,
|
||||
"narHash": "sha256-vUfIeBEfpbAfZ5zjgIkYk7eHBeVfCYVjLbWnMkseYnk=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b",
|
||||
"rev": "643809054d65fdd466a63e3155b8c498cb483c04",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
120
go.mod
120
go.mod
|
|
@ -4,61 +4,62 @@ go 1.26.0
|
|||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0
|
||||
github.com/anthropics/anthropic-sdk-go v1.61.0
|
||||
github.com/anthropics/anthropic-sdk-go v1.67.0
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrock v1.66.0
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.56.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.44.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.40
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrock v1.68.0
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.58.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.15
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/go-git/go-git/v5 v5.19.1
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/go-shiori/go-readability v0.0.0-20251205110129-5db1dc9836f0
|
||||
github.com/google/go-github/v66 v66.0.0
|
||||
github.com/hasura/go-graphql-client v0.16.0
|
||||
github.com/jessevdk/go-flags v1.6.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-sqlite3 v1.14.48
|
||||
github.com/mattn/go-sqlite3 v1.14.50
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/ollama/ollama v0.32.3
|
||||
github.com/ollama/ollama v0.33.1
|
||||
github.com/openai/openai-go v1.12.0
|
||||
github.com/otiai10/copy v1.14.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/samber/lo v1.53.0
|
||||
github.com/sgaunet/perplexity-go/v2 v2.16.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/text v0.40.0
|
||||
google.golang.org/api v0.290.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/text v0.41.0
|
||||
google.golang.org/api v0.294.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.9.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.6.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
||||
github.com/go-openapi/spec v0.22.9 // indirect
|
||||
github.com/go-openapi/swag/conv v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/loading v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/pools v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.1 // indirect
|
||||
github.com/go-openapi/spec v0.22.11 // indirect
|
||||
github.com/go-openapi/swag/conv v0.29.1 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.29.1 // indirect
|
||||
github.com/go-openapi/swag/loading v0.29.1 // indirect
|
||||
github.com/go-openapi/swag/pools v0.29.1 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.29.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.29.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.29.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
|
|
@ -72,18 +73,18 @@ require (
|
|||
github.com/quic-go/quic-go v0.61.0 // indirect
|
||||
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.2 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
|
||||
golang.org/x/mod v0.38.0 // indirect
|
||||
golang.org/x/mod v0.40.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.22.0 // indirect
|
||||
cloud.google.com/go/auth v0.23.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
|
|
@ -91,20 +92,20 @@ require (
|
|||
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.4 // indirect
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 // indirect
|
||||
github.com/aws/smithy-go v1.27.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.39
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 // indirect
|
||||
github.com/aws/smithy-go v1.28.1 // indirect
|
||||
github.com/bytedance/sonic v1.15.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.2 // indirect
|
||||
github.com/cloudflare/circl v1.6.5 // indirect
|
||||
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||
github.com/coder/websocket v1.8.15 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
|
||||
|
|
@ -126,8 +127,8 @@ require (
|
|||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.19 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.23.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.21 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.24.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
|
|
@ -149,21 +150,20 @@ require (
|
|||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.2 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
golang.org/x/arch v0.29.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect
|
||||
go.opentelemetry.io/otel v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.46.0 // indirect
|
||||
golang.org/x/arch v0.30.0 // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
google.golang.org/genai v1.65.0
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df // indirect
|
||||
google.golang.org/grpc v1.82.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
google.golang.org/genai v1.70.0
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
|
|
|||
125
go.sum
125
go.sum
|
|
@ -2,6 +2,8 @@ cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
|||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.22.0 h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ=
|
||||
cloud.google.com/go/auth v0.22.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s=
|
||||
cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo=
|
||||
cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
|
|
@ -10,6 +12,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
|||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 h1:4gRPBpN1f6xt88yi4WR26m7XaD9OlWtVT6bWPdGUIok=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0/go.mod h1:G7QVLxw1j1JVyrO1MA95S8m8HStaaleDZYTcfGgjB2o=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs=
|
||||
|
|
@ -20,6 +24,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ
|
|||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.9.0 h1:MDT4FxAPve5FnYn6vOL1r7RCRDG+l9cI7a5LlCuHsqA=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.9.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
|
|
@ -35,6 +41,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
|||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/anthropics/anthropic-sdk-go v1.61.0 h1:JRTnm1tPqn5xo1xd1zfrcFDlcoWXVMvV1K68YmhpZKw=
|
||||
github.com/anthropics/anthropic-sdk-go v1.61.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI=
|
||||
github.com/anthropics/anthropic-sdk-go v1.67.0 h1:tTsVSR+YD6R+1EPoMyZ9im7YKEjJpYGZk5klcDi9HRg=
|
||||
github.com/anthropics/anthropic-sdk-go v1.67.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
|
|
@ -43,52 +51,94 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
|||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0 h1:fharf/WhbRAVZ1du0QL7roNFxZ6T/sWr+4Ni617bwSI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.44.0 h1:4IbaHhtzy+4h37z4JQyO9a2QsiCml3CNYHtq5hIHigo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.44.0/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14/go.mod h1:zwM6veDkhGgQFqkBy+uT28AAYpLu+uFMlPl+rCg/73E=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31 h1:n4nY9O3QKoHIkL85EX+V8RcMFtOhlpTFhGArg915PXk=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31/go.mod h1:PN0NYDCCoOpGGsZ2+elDUidmHfQBPyYzN2GCgl8HEBs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.40 h1:lAVC9gMmKusmqDRe32dPtgKl/BWvJmMJoWELKHCAObw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.40/go.mod h1:8xOJLbe/hOj1g4PVsfJYV7O2byq+UGET1onDdUgbwqc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30 h1:TTCvvzFU6gXa4iJecNG/0F/B0oYTiazoRECr2XyLHrY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30/go.mod h1:jKxAp2AEncnliinzpgOSZDFv6+VjvWhjw/AtbfsWT9U=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.39 h1:XOg8LC3Kgnsa3WiPQjc7Bi8k5IBN92cPYfIV9XMFss0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.39/go.mod h1:GonTDBQ+mTpCVNwaHjj0PagspfrYYMEqOx7FehoEP/I=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 h1:kfVL5wAunCJycL6MOQ6aNh6PlAYEymflcjuKmrWUA0o=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31/go.mod h1:nWfRNDAppujCQgOUd43lKT4yeLv9z3nJ3bw1G3BgQKo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 h1:r5aGipEVgI9aT/tAGjdrPbDQvIAKdTrS3rUPQtG4Rmo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40/go.mod h1:vOD3CnPxAdkL6MWZeROkZsTlskklMFfgVFkHzx/oZpY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 h1:Z8F3hfCY33IGpJjFAnv0wvtv1FIKj1GHmRDEYqy64tw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31/go.mod h1:aVyUoytEyOViR6jhq6jula0xkc5NfBE2hgeF6BvOrao=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 h1:UIXlbijuB2XK1Kr57fo8iIxCuaSHJzwZ1uo+2tbEYIk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40/go.mod h1:wcEsL6jscjZjVUinb0Q5qD/GXOG1yT3GNfmT9HuDwzU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 h1:hyOxUyXdh3AyjE93gBgsfziJag9ACwcs+ZpDBLzi8mw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31/go.mod h1:OERqI9k0draSLB8O8woxY3q25ZWTELRK4RRoLMuMZFo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 h1:xLQVRDs2NddDmK9BEyh5KSlJ1Gpy5/GIJXrV6WcVGAE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40/go.mod h1:XRXnpFVFGLaEVK+olDdFIM1vNa04ETW452oFGEPUxAo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 h1:0MrUL35H/Y4kdFfItoR5jCgtDQ4Z/8LudAoIHRfA4hE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32/go.mod h1:2tNZkuWz54arj8mHVf+8Y7cKkcD8Wr/fBpENgEXpjLc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 h1:nv/ILuCY0yXACzMQwvtt/HbqDDjemZiI0AeDbxGQlnU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41/go.mod h1:dzvOSpxaPqQ3j0xS6Lc1vyVuWW0RBj7s/QqYpzu3Q/0=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrock v1.66.0 h1:RMRC4k42Bt3wHqWiPyFfiRrOyRhJZW7w6vAw5ZnMrPM=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrock v1.66.0/go.mod h1:1asCJhndokTo9QEJ68/eZv4yJsRPV9A0ROdDx7QQBkQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrock v1.68.0 h1:2T9SsmNe78eLtmobdnvxk088DVuUfYp9Ml9MxFMWQUQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrock v1.68.0/go.mod h1:ANOxqA/+nqohBqirnFCF8EqJFkjAbTLYfyBl6NEjNug=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.56.0 h1:CWw8zDpnMJLwSvZd41Ncf/eJPeZ5t74UxGAu4HbM3S4=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.56.0/go.mod h1:dGxTgK2ZKWrbZv5o/8oCeO3Uch3n0w2rtSFroeJoLcE=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.58.0 h1:TDwZrhBZTHNxvGiqqDoNjdUuoveRRVfy14VeFHbbWBc=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.58.0/go.mod h1:ZnrFfnjYjXc/PC2a2hwAIS2qf1Yqk15EMLryhca2wps=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 h1:w2SIhW92DZPFrSL4ksVCr8IYff5OZwIcxg8+95tzvAI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31/go.mod h1:wAhpCQbkov+IcvjozJbd2xRCoZybUEHNkcFunssNACg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40 h1:gr3Fw1cxZXNCdeo/lQ7isHEHzvHVM7z75qb2zW9aMjw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40/go.mod h1:8z/9CmfnQhiuXD7Ykbcg4a/whSWsniE0ODSx9uwVzfk=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 h1:OHH5iTQvVGmfHjX/5Q+vFuA/Rf2x6/95aJ/75QCQSm4=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0/go.mod h1:mCF3AK9PpL49oOrhniUXWAfhVBVQ/XbytoE5eccZUIs=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 h1:agcr0j8YeFEzdXNo17Rg9MbbjLRjrimabwNtji4e+lU=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.6.0/go.mod h1:qU5PxgQ4JiUOOMotzfO3+5oUda5W+8JDVKyLQqlrJik=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 h1:CaJyYhxBE0M/HJX/YvSaSmQlsI91VHB0lKU8LtLxL3A=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0/go.mod h1:+e6BMRMPjBQoCw/WovYR9GLy2IU0z4Q77smOB1DraSg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 h1:FxaN8/sn61DTXNI6Gt678tFJUY8iUsCchm6Y/F/RjaA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.34.0/go.mod h1:vu4OY6s8LJtT8BtYG2LD6BGSZMptkYn3o5hvCPB22jc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 h1:tC323YV77QdafeBr6LUhLDTsboyuyHLNRwAyCP44kGU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0/go.mod h1:SfLK1sgviHmbI+MozR9iDwDjL4cdCVZtahsjoR+z7wg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 h1:crWKPeGYTBTuBxQ3p73kjfJvt4brUIsr+Fuypko8FxY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0/go.mod h1:HjjZVhaBz0JBR/kbWKThmNDhFKS7y6EURuk493tJk9Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 h1:Pd6PNlp4t8PTXxqzstICl52Wsy78vpjFZ7PRUj44mJc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0/go.mod h1:rmQ0TnHzuLPmabgjPcsywhsSOmaBDgzR4zvDxSPsGdg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 h1:IZ63JdogSNNjex/jsODNv7jGDcO/xJYd9FsgyfCsp1g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.46.0/go.mod h1:I+rwAf3spG5dITBaAo3xXRowk8kiOhtU1kYxfvCTC44=
|
||||
github.com/aws/smithy-go v1.27.4 h1:JQcphmBN4f0q/sPqXqROIItRNV/hy10cgu7CsFy616M=
|
||||
github.com/aws/smithy-go v1.27.4/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ=
|
||||
github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g=
|
||||
github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/buger/jsonparser v1.6.1 h1:I0phFv0PlbLHnM7TZAVjZ2MJ2/eWRTDyuO7GLR98IEs=
|
||||
github.com/buger/jsonparser v1.6.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
|
||||
github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
|
||||
github.com/bytedance/sonic v1.15.3 h1:P3akjLPBtV/i6bHC6LbcLjY3KuoOvfiqF8wFHeP5IhY=
|
||||
github.com/bytedance/sonic v1.15.3/go.mod h1:8e51yTPdY8M6t+vvGL1c2Y1xL9i+frEeIAQAEl75NUc=
|
||||
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo=
|
||||
github.com/bytedance/sonic/loader v0.5.2/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
|
||||
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
|
||||
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
||||
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
|
||||
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
|
||||
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
|
|
@ -133,6 +183,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
|
|||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
|
|
@ -142,29 +194,50 @@ github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxg
|
|||
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
|
||||
github.com/go-openapi/jsonreference v1.0.1 h1:4zJ7AmYDKNmD3aSpfPnFNCFA5E80/xMHUNKgydaLh38=
|
||||
github.com/go-openapi/jsonreference v1.0.1/go.mod h1:dYplQXa6p5lXprLcJ8LE2iU7vNpXsAHDQ5ZAgL+Qx3A=
|
||||
github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w=
|
||||
github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0=
|
||||
github.com/go-openapi/spec v0.22.11 h1:/ZW28n4PCghbBpwjmvoMeyCeDwd6pIe715Ov8T3kAaQ=
|
||||
github.com/go-openapi/spec v0.22.11/go.mod h1:Ypu+u1KTejey7/QCr3j1ChdjwhGgXuklbH7l8+YP1J4=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU=
|
||||
github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ=
|
||||
github.com/go-openapi/swag/conv v0.29.1 h1:AC4Eh/5c/eUDOUCzzsRC9ghmFgOSBHeRMGIngY0ZUGA=
|
||||
github.com/go-openapi/swag/conv v0.29.1/go.mod h1:S1X7/ZrBEZOC0Wc8AGxjbcGS92l3WEjA7aPtpl+RaqM=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo=
|
||||
github.com/go-openapi/swag/jsonutils v0.29.1 h1:AFCxs0eQZ24/QyfhVHM2t49rMz7Vv3XCsZQI6yrNy+c=
|
||||
github.com/go-openapi/swag/jsonutils v0.29.1/go.mod h1:u3+sCfJpttDpcmS5kpm0yxL6GK0eWgODsx8Yw8fcqNM=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.1 h1:BiiXE31Bx9SfpsMmOQj5KYpUhTZBpLVriVhJDuLuY2o=
|
||||
github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0=
|
||||
github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE=
|
||||
github.com/go-openapi/swag/loading v0.29.1 h1:FCv5fG8UhTdDJa2R7w+5O9Ekpcbw7tt0nFWvmDKGBjc=
|
||||
github.com/go-openapi/swag/loading v0.29.1/go.mod h1:N0ESuem4p2oedKal8EJhciqnJ9Q9Wmt83L1CRB3Fouw=
|
||||
github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk=
|
||||
github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
|
||||
github.com/go-openapi/swag/pools v0.29.1 h1:NRogYxdEW9SjRM4mkAOji9iefO4MRXq3p/ZJcoQbUKg=
|
||||
github.com/go-openapi/swag/pools v0.29.1/go.mod h1:leDcaghjkRAhCuCRv9NfJU5f0mjoU3cT/XZObhMk3pc=
|
||||
github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI=
|
||||
github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||
github.com/go-openapi/swag/stringutils v0.29.1 h1:1ykunK7iJQk1uOO7+oUH1ukbsK85fFCOiCFMOVSY+F0=
|
||||
github.com/go-openapi/swag/stringutils v0.29.1/go.mod h1:7fSqZ+z8Qc0tOfAAK0jVa5qFGrnIlRi6n7NeGGrr1vc=
|
||||
github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4=
|
||||
github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||
github.com/go-openapi/swag/typeutils v0.29.1 h1:Nzv9nhnlLCRBPQqfOX+7lB6Guju370or8StT+lIOf6M=
|
||||
github.com/go-openapi/swag/typeutils v0.29.1/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU=
|
||||
github.com/go-openapi/swag/yamlutils v0.29.1 h1:69w3tsBajm7MR/fejLy7HD/3J68Ys1SeeZMEzZ3w2sk=
|
||||
github.com/go-openapi/swag/yamlutils v0.29.1/go.mod h1:rgsp3vT/QdWzKwn43CigDwjOGIenPyTZMKnxEM8jZOA=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGLGREDYXG56Xr4gMjEtE=
|
||||
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
|
||||
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/go-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
|
|
@ -203,8 +276,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.19 h1:mMOE7DN2+p76/EdIrmAy9B9bH+yC4563vmnJ34QR8i4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.19/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.21 h1:OFdQ3tnCX/zaQ0Cedur3D3z7kI6HiLX9g3TiAN4/DFU=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.21/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
|
||||
github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
|
||||
github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
|
||||
github.com/googleapis/gax-go/v2 v2.24.0 h1:myMaPYyF9MecEmvQqMqomIwn9t/4KCZN9qnwsS76wlg=
|
||||
github.com/googleapis/gax-go/v2 v2.24.0/go.mod h1:IaTHBDd7NHxSCiu0vEs8pQZu4dGZrWwuSoxCnk16OFM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hasura/go-graphql-client v0.16.0 h1:DQLfp+djj4j5NPdJkGYym8J55hpm5etML1zqgco78Qc=
|
||||
|
|
@ -247,6 +324,8 @@ github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI
|
|||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs=
|
||||
github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/mattn/go-sqlite3 v1.14.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY=
|
||||
github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
|
@ -256,6 +335,8 @@ github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd
|
|||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||
github.com/ollama/ollama v0.32.3 h1:hASAqO6McQAgOhLG3Fcgrj//aXK5rMMWxqFM4eAKkGc=
|
||||
github.com/ollama/ollama v0.32.3/go.mod h1:b1ydCt2oVg0VAg22WWDgCbwW0AyOaRKAFzlS91NI4OY=
|
||||
github.com/ollama/ollama v0.33.1 h1:nEeqiZzqlO+RfjJ7AvfUgrYlV/wbrxPl2szRg9mtxPU=
|
||||
github.com/ollama/ollama v0.33.1/go.mod h1:Kekx/+OtFZHmqbkVH/QUUDVcMQS+1pg1dcz4Qy7TGn4=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0=
|
||||
|
|
@ -320,6 +401,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||
|
|
@ -341,6 +424,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ugorji/go/codec v1.3.2 h1:zkEASHHyEClGeURfgNT9PJZVfAbs9oEX9QXggwWNJbc=
|
||||
github.com/ugorji/go/codec v1.3.2/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
|
|
@ -348,40 +433,62 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.2 h1:b6o2m7zL8g2URuO8urBedAylxojybKXNZTxgkOcl+2w=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.2/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 h1:B2h3uqicet1CT2N5TOFhS+Gq++9i0/CLmaxvhmhtP5s=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0/go.mod h1:dylvB+ZiiwMvsDij9O84Uy7SijLgHMX4mbkncds+4Sw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
|
||||
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
|
||||
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
|
||||
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho=
|
||||
golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/arch v0.30.0 h1:sB9h+1gRGa2+LauFSV0tm8bK1J2yo1bx6/Uyi/P6DTU=
|
||||
golang.org/x/arch v0.30.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
|
@ -389,6 +496,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -420,6 +529,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
@ -427,23 +538,37 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/api v0.290.0 h1:eMw0Xo+IfbbMlKmW7aHvpyQRv9RCXuWx/vs8AD+0x9A=
|
||||
google.golang.org/api v0.290.0/go.mod h1:weJZ3lldHFYI0DBFNKpJelUDNnusTt5YaOEgxvt8ci8=
|
||||
google.golang.org/api v0.294.0 h1:8gASjJxdtcIieB3OqbkLcF0FfbXVNqKtU5iozD1ssvA=
|
||||
google.golang.org/api v0.294.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4=
|
||||
google.golang.org/genai v1.65.0 h1:6QK3Rjsx0iuJjbDCE1vf1VUr1IEjRDpvexGGnhxoAIk=
|
||||
google.golang.org/genai v1.65.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU=
|
||||
google.golang.org/genai v1.70.0 h1:V9oYOBvTDYbmeklOHjvxGeeLau71WiS2CWKrktQtDok=
|
||||
google.golang.org/genai v1.70.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
||||
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d h1:QwnJwPte4XXAkhPu26LTDIahnsMSUV0kK8HkxbC+Pc4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ type Flags struct {
|
|||
YouTubePlaylist bool `long:"playlist" description:"Prefer playlist over video if both ids are present in the URL"`
|
||||
YouTubeTranscript bool `long:"transcript" description:"Grab transcript from YouTube video and send to chat (it is used per default)."`
|
||||
YouTubeTranscriptWithTimestamps bool `long:"transcript-with-timestamps" description:"Grab transcript from YouTube video with timestamps and send to chat"`
|
||||
YouTubeVisual bool `long:"visual"`
|
||||
YouTubeVisualSensitivity float64 `long:"visual-sensitivity" default:"0.4"`
|
||||
YouTubeVisualFps int `long:"visual-fps" default:"0"`
|
||||
YouTubeVisual bool `long:"visual" description:"Extract visual data from video using OCR and FFmpeg"`
|
||||
YouTubeVisualSensitivity float64 `long:"visual-sensitivity" description:"Tolerance for FFmpeg scene detection (0.0 - 1.0)" default:"0.4"`
|
||||
YouTubeVisualFps int `long:"visual-fps" description:"Extract a specific number of frames per second instead of using scene detection" default:"0"`
|
||||
YouTubeComments bool `long:"comments" description:"Grab comments from YouTube video and send to chat"`
|
||||
YouTubeMetadata bool `long:"metadata" description:"Output video metadata"`
|
||||
YtDlpArgs string `long:"yt-dlp-args" yaml:"ytDlpArgs" description:"Additional arguments to pass to yt-dlp (e.g. '--cookies-from-browser brave')"`
|
||||
|
|
@ -78,8 +78,8 @@ type Flags struct {
|
|||
DryRun bool `long:"dry-run" description:"Show what would be sent to the model without actually sending it"`
|
||||
Serve bool `long:"serve" description:"Serve the Fabric Rest API"`
|
||||
ServeOllama bool `long:"serveOllama" description:"Serve the Fabric Rest API with ollama endpoints"`
|
||||
ServeAddress string `long:"address" description:"The address to bind the REST API" default:":8080"`
|
||||
ServeAPIKey string `long:"api-key" description:"API key used to secure server routes" default:""`
|
||||
ServeAddress string `long:"address" description:"The address to bind the REST API" default:"127.0.0.1:8080"`
|
||||
ServeAPIKey string `long:"api-key" env:"FABRIC_API_KEY" description:"API key used to secure server routes" default:""`
|
||||
Config string `long:"config" description:"Path to YAML config file"`
|
||||
Version bool `long:"version" description:"Print current version"`
|
||||
ListExtensions bool `long:"listextensions" description:"List all registered extensions"`
|
||||
|
|
@ -109,7 +109,7 @@ type Flags struct {
|
|||
Notification bool `long:"notification" yaml:"notification" description:"Send desktop notification when command completes"`
|
||||
NotificationCommand string `long:"notification-command" yaml:"notificationCommand" description:"Custom command to run for notifications (overrides built-in notifications)"`
|
||||
Thinking domain.ThinkingLevel `long:"thinking" yaml:"thinking" description:"Set reasoning/thinking level (e.g., off, low, medium, high, or numeric tokens for Anthropic or Google Gemini)"`
|
||||
ShowMetadata bool `long:"show-metadata" description:"Print metadata to stderr"`
|
||||
ShowMetadata bool `long:"show-metadata" description:"Print metadata (input/output tokens) to stderr"`
|
||||
Debug int `long:"debug" description:"Set debug level (0=off, 1=basic, 2=detailed, 3=trace, 4=wire)" default:"0"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ var flagDescriptionMap = map[string]string{
|
|||
"raw": "use_model_defaults_raw_help",
|
||||
"frequencypenalty": "set_frequency_penalty",
|
||||
"listpatterns": "list_all_patterns",
|
||||
"readpattern": "print_pattern_contents",
|
||||
"listmodels": "list_all_available_models",
|
||||
"listcontexts": "list_all_contexts",
|
||||
"listsessions": "list_all_sessions",
|
||||
|
|
@ -49,6 +50,7 @@ var flagDescriptionMap = map[string]string{
|
|||
"comments": "grab_comments_from_youtube",
|
||||
"metadata": "output_video_metadata",
|
||||
"yt-dlp-args": "additional_yt_dlp_args",
|
||||
"spotify": "spotify_url_help",
|
||||
"language": "specify_language_code",
|
||||
"scrape_url": "scrape_website_url",
|
||||
"scrape_question": "search_question_jina",
|
||||
|
|
@ -94,6 +96,7 @@ var flagDescriptionMap = map[string]string{
|
|||
"notification": "send_desktop_notification",
|
||||
"notification-command": "custom_notification_command",
|
||||
"thinking": "set_reasoning_thinking_level",
|
||||
"show-metadata": "print_metadata_to_stderr",
|
||||
"debug": "set_debug_level",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ import (
|
|||
restapi "github.com/danielmiessler/fabric/internal/server"
|
||||
)
|
||||
|
||||
// serveOllama is a seam for tests, because the real entry point blocks
|
||||
// on a listening socket.
|
||||
var serveOllama = restapi.ServeOllama
|
||||
|
||||
// handleSetupAndServerCommands handles setup and server-related commands
|
||||
// Returns (handled, error) where handled indicates if a command was processed and should exit
|
||||
func handleSetupAndServerCommands(currentFlags *Flags, registry *core.PluginRegistry, version string) (handled bool, err error) {
|
||||
|
|
@ -22,7 +26,7 @@ func handleSetupAndServerCommands(currentFlags *Flags, registry *core.PluginRegi
|
|||
|
||||
if currentFlags.ServeOllama {
|
||||
registry.ConfigureVendors()
|
||||
err = restapi.ServeOllama(registry, currentFlags.ServeAddress, version)
|
||||
err = serveOllama(registry, currentFlags.ServeAddress, version, currentFlags.ServeAPIKey)
|
||||
return true, err
|
||||
}
|
||||
|
||||
|
|
|
|||
37
internal/cli/setup_server_test.go
Normal file
37
internal/cli/setup_server_test.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/core"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/ai"
|
||||
)
|
||||
|
||||
// The --serveOllama path must pass the address, version, and API key
|
||||
// flags through to ServeOllama unchanged.
|
||||
func TestHandleSetupAndServerCommands_ServeOllamaWiring(t *testing.T) {
|
||||
var gotAddress, gotVersion, gotKey string
|
||||
prev := serveOllama
|
||||
serveOllama = func(_ *core.PluginRegistry, address, version, apiKey string) error {
|
||||
gotAddress, gotVersion, gotKey = address, version, apiKey
|
||||
return nil
|
||||
}
|
||||
defer func() { serveOllama = prev }()
|
||||
|
||||
registry := &core.PluginRegistry{
|
||||
VendorManager: ai.NewVendorsManager(),
|
||||
VendorsAll: ai.NewVendorsManager(),
|
||||
}
|
||||
flags := &Flags{ServeOllama: true, ServeAddress: "127.0.0.1:9999", ServeAPIKey: "secret"}
|
||||
handled, err := handleSetupAndServerCommands(flags, registry, "v-test")
|
||||
if err != nil {
|
||||
t.Fatalf("handleSetupAndServerCommands() error = %v", err)
|
||||
}
|
||||
if !handled {
|
||||
t.Fatal("handleSetupAndServerCommands() handled = false, want true")
|
||||
}
|
||||
if gotAddress != "127.0.0.1:9999" || gotVersion != "v-test" || gotKey != "secret" {
|
||||
t.Fatalf("ServeOllama got (%q, %q, %q), want (127.0.0.1:9999, v-test, secret)",
|
||||
gotAddress, gotVersion, gotKey)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
debuglog "github.com/danielmiessler/fabric/internal/log"
|
||||
|
|
@ -73,6 +74,24 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) {
|
|||
vendors := []ai.Vendor{}
|
||||
|
||||
// Add non-OpenAI compatible clients
|
||||
codexClient := codex.NewClient()
|
||||
codexClient.WithStoreLock = func(fn func() error) error {
|
||||
return db.WithEnvLock(func() error {
|
||||
env, err := db.ReadEnvFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
codexClient.LoadEnvSettings(env)
|
||||
return fn()
|
||||
})
|
||||
}
|
||||
codexClient.TokenPersist = func() error {
|
||||
return db.ApplyEnvUpdates(map[string]string{
|
||||
codexClient.AccessToken.EnvVariable: strings.TrimSpace(codexClient.AccessToken.Value),
|
||||
codexClient.RefreshToken.EnvVariable: strings.TrimSpace(codexClient.RefreshToken.Value),
|
||||
codexClient.AccountID.EnvVariable: strings.TrimSpace(codexClient.AccountID.Value),
|
||||
})
|
||||
}
|
||||
vendors = append(vendors,
|
||||
openai.NewClient(),
|
||||
digitalocean.NewClient(),
|
||||
|
|
@ -86,7 +105,7 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) {
|
|||
lmstudio.NewClient(),
|
||||
exolab.NewClient(),
|
||||
perplexity.NewClient(),
|
||||
codex.NewClient(),
|
||||
codexClient,
|
||||
copilot.NewClient(), // Microsoft 365 Copilot
|
||||
bedrock.NewClient(), // AWS Bedrock - credentials configured via setup or AWS credential chain
|
||||
)
|
||||
|
|
@ -123,6 +142,8 @@ func (o *PluginRegistry) ListVendors(out io.Writer) error {
|
|||
type PluginRegistry struct {
|
||||
Db *fsdb.Db
|
||||
|
||||
vendorMu sync.Mutex
|
||||
|
||||
VendorManager *ai.VendorsManager
|
||||
VendorsAll *ai.VendorsManager
|
||||
Defaults *tools.Defaults
|
||||
|
|
@ -288,16 +309,12 @@ func (o *PluginRegistry) runVendorSetup() (err error) {
|
|||
return pluginSetupErr
|
||||
}
|
||||
|
||||
o.registerVendor(plugin)
|
||||
|
||||
if err = o.SaveEnvFile(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if o.VendorManager.FindByName(plugin.GetName()) == nil {
|
||||
if vendor, ok := plugin.(ai.Vendor); ok {
|
||||
o.VendorManager.AddVendors(vendor)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -349,16 +366,11 @@ func (o *PluginRegistry) runInteractiveSetup() (err error) {
|
|||
if pluginSetupErr := plugin.Setup(); pluginSetupErr != nil {
|
||||
println(pluginSetupErr.Error())
|
||||
} else {
|
||||
o.registerVendor(plugin)
|
||||
if err = o.SaveEnvFile(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if o.VendorManager.FindByName(plugin.GetName()) == nil {
|
||||
if vendor, ok := plugin.(ai.Vendor); ok {
|
||||
o.VendorManager.AddVendors(vendor)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
|
@ -425,11 +437,32 @@ func (o *PluginRegistry) SetupVendor(vendorName string) (err error) {
|
|||
if err = o.VendorsAll.SetupVendor(vendorName, o.VendorManager.VendorsByName); err != nil {
|
||||
return
|
||||
}
|
||||
if vendor := o.VendorsAll.FindByName(vendorName); vendor != nil {
|
||||
o.registerVendor(vendor)
|
||||
}
|
||||
err = o.SaveEnvFile()
|
||||
return
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) registerVendor(plugin plugins.Plugin) {
|
||||
vendor, ok := plugin.(ai.Vendor)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
o.vendorMu.Lock()
|
||||
defer o.vendorMu.Unlock()
|
||||
name := vendor.GetName()
|
||||
for _, existing := range o.VendorManager.Vendors {
|
||||
if strings.EqualFold(existing.GetName(), name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
o.VendorManager.AddVendors(vendor)
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) ConfigureVendors() {
|
||||
o.vendorMu.Lock()
|
||||
defer o.vendorMu.Unlock()
|
||||
o.VendorManager.Clear()
|
||||
for _, vendor := range o.VendorsAll.Vendors {
|
||||
if vendorErr := vendor.Configure(); vendorErr == nil && vendor.IsConfigured() {
|
||||
|
|
@ -438,6 +471,34 @@ func (o *PluginRegistry) ConfigureVendors() {
|
|||
}
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) activateVendor(name string) (ai.Vendor, error) {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
o.vendorMu.Lock()
|
||||
defer o.vendorMu.Unlock()
|
||||
|
||||
if v := o.VendorManager.FindByName(name); v != nil {
|
||||
return v, nil
|
||||
}
|
||||
if o.VendorsAll == nil {
|
||||
return nil, nil
|
||||
}
|
||||
v := o.VendorsAll.FindByName(name)
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := v.Configure(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !v.IsConfigured() {
|
||||
return nil, nil
|
||||
}
|
||||
o.VendorManager.AddVendors(v)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (o *PluginRegistry) GetModels() (ret *ai.VendorsModels, err error) {
|
||||
o.ConfigureVendors()
|
||||
ret, err = o.VendorManager.GetModels()
|
||||
|
|
@ -503,13 +564,25 @@ func (o *PluginRegistry) GetChatter(model string, modelContextLength int, vendor
|
|||
ret.model = defaultModel
|
||||
}
|
||||
} else if model == "" {
|
||||
if vendorName != "" {
|
||||
ret.vendor = vendorManager.FindByName(vendorName)
|
||||
} else {
|
||||
ret.vendor = vendorManager.FindByName(defaultVendor)
|
||||
name := vendorName
|
||||
if name == "" {
|
||||
name = defaultVendor
|
||||
}
|
||||
if ret.vendor, err = o.activateVendor(name); err != nil {
|
||||
return
|
||||
}
|
||||
ret.model = defaultModel
|
||||
} else {
|
||||
if vendorName != "" {
|
||||
if ret.vendor, err = o.activateVendor(vendorName); err != nil {
|
||||
return
|
||||
}
|
||||
} else if !vendorManager.HasVendors() {
|
||||
if _, err = o.activateVendor(defaultVendor); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var models *ai.VendorsModels
|
||||
if models, err = vendorManager.GetModels(); err != nil {
|
||||
return
|
||||
|
|
@ -567,6 +640,16 @@ func (o *PluginRegistry) GetChatter(model string, modelContextLength int, vendor
|
|||
ret.model = model
|
||||
}
|
||||
|
||||
if ret.vendor == nil {
|
||||
name := vendorName
|
||||
if name == "" {
|
||||
name = defaultVendor
|
||||
}
|
||||
if ret.vendor, err = o.activateVendor(name); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ret.vendor == nil {
|
||||
var errMsg string
|
||||
if defaultModel == "" || defaultVendor == "" {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package core
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
|
@ -10,11 +11,14 @@ import (
|
|||
|
||||
"github.com/danielmiessler/fabric/internal/chat"
|
||||
"github.com/danielmiessler/fabric/internal/domain"
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
debuglog "github.com/danielmiessler/fabric/internal/log"
|
||||
"github.com/danielmiessler/fabric/internal/plugins"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/ai"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/ai/codex"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
|
||||
"github.com/danielmiessler/fabric/internal/tools"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func TestSaveEnvFile(t *testing.T) {
|
||||
|
|
@ -32,14 +36,15 @@ func TestSaveEnvFile(t *testing.T) {
|
|||
|
||||
// testVendor implements ai.Vendor for testing purposes
|
||||
type testVendor struct {
|
||||
name string
|
||||
models []string
|
||||
name string
|
||||
models []string
|
||||
configureErr error
|
||||
}
|
||||
|
||||
func (m *testVendor) GetName() string { return m.name }
|
||||
func (m *testVendor) GetSetupDescription() string { return m.name }
|
||||
func (m *testVendor) IsConfigured() bool { return true }
|
||||
func (m *testVendor) Configure() error { return nil }
|
||||
func (m *testVendor) Configure() error { return m.configureErr }
|
||||
func (m *testVendor) Setup() error { return nil }
|
||||
func (m *testVendor) SetupFillEnvFileContent(*bytes.Buffer) {}
|
||||
func (m *testVendor) ListModels(context.Context) ([]string, error) { return m.models, nil }
|
||||
|
|
@ -210,3 +215,116 @@ func TestGetChatter_VendorPrefixIgnoredWhenNotAVendor(t *testing.T) {
|
|||
t.Fatalf("expected model 'notavendor/model', got %s", chatter.model)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChatter_ReportsConfigureError(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
for _, tt := range []struct {
|
||||
name, model, vendor string
|
||||
}{
|
||||
{"empty model", "", ""},
|
||||
{"model", "gpt-5.6-sol", ""},
|
||||
{"named vendor", "gpt-5.6-sol", "Codex"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
configureErr := errors.New(i18n.T("codex_login_refresh_failed"))
|
||||
registry := newInactiveVendorRegistry(t, "Codex", "gpt-5.6-sol", &testVendor{
|
||||
name: "Codex", models: []string{"gpt-5.6-sol"}, configureErr: configureErr,
|
||||
})
|
||||
_, err := registry.GetChatter(tt.model, 0, tt.vendor, false, false)
|
||||
if !errors.Is(err, configureErr) {
|
||||
t.Fatalf("GetChatter() error = %v, want %v", err, configureErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChatter_ActivatesInactiveVendor(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name, model, vendor string
|
||||
}{
|
||||
{"named", "gpt-5.6-sol", "Codex"},
|
||||
{"default for model", "gpt-5.6-sol", ""},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
codexVendor := &testVendor{name: "Codex", models: []string{"gpt-5.6-sol"}}
|
||||
registry := newInactiveVendorRegistry(t, "Codex", "gpt-5.6-sol", codexVendor)
|
||||
chatter, err := registry.GetChatter(tt.model, 0, tt.vendor, false, false)
|
||||
if err != nil {
|
||||
t.Fatalf("GetChatter() error = %v", err)
|
||||
}
|
||||
if chatter.vendor.GetName() != "Codex" {
|
||||
t.Fatalf("vendor = %s, want Codex", chatter.vendor.GetName())
|
||||
}
|
||||
if chatter.model != "gpt-5.6-sol" {
|
||||
t.Fatalf("model = %s, want gpt-5.6-sol", chatter.model)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPluginRegistry_CodexTokenPersist(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db := fsdb.NewDb(dir)
|
||||
if err := db.SaveEnv("KEEP=old\n"); err != nil {
|
||||
t.Fatalf("SaveEnv() error = %v", err)
|
||||
}
|
||||
|
||||
registry, err := NewPluginRegistry(db)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPluginRegistry() error = %v", err)
|
||||
}
|
||||
|
||||
vendor := registry.VendorsAll.FindByName("Codex")
|
||||
client, ok := vendor.(*codex.Client)
|
||||
if !ok || client == nil {
|
||||
t.Fatal("expected Codex client in VendorsAll")
|
||||
}
|
||||
if client.TokenPersist == nil {
|
||||
t.Fatal("TokenPersist is nil")
|
||||
}
|
||||
|
||||
client.AccessToken.Value = "access-live"
|
||||
client.RefreshToken.Value = "refresh-live"
|
||||
client.AccountID.Value = "acct-live"
|
||||
if err := client.TokenPersist(); err != nil {
|
||||
t.Fatalf("TokenPersist() error = %v", err)
|
||||
}
|
||||
|
||||
parsed, err := godotenv.Read(db.EnvFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("godotenv.Read() error = %v", err)
|
||||
}
|
||||
if parsed["KEEP"] != "old" {
|
||||
t.Fatalf("KEEP = %q, want old", parsed["KEEP"])
|
||||
}
|
||||
if parsed[client.AccessToken.EnvVariable] != "access-live" {
|
||||
t.Fatalf("access = %q, want access-live", parsed[client.AccessToken.EnvVariable])
|
||||
}
|
||||
if parsed[client.RefreshToken.EnvVariable] != "refresh-live" {
|
||||
t.Fatalf("refresh = %q, want refresh-live", parsed[client.RefreshToken.EnvVariable])
|
||||
}
|
||||
if parsed[client.AccountID.EnvVariable] != "acct-live" {
|
||||
t.Fatalf("account = %q, want acct-live", parsed[client.AccountID.EnvVariable])
|
||||
}
|
||||
}
|
||||
|
||||
func newInactiveVendorRegistry(t *testing.T, defaultVendor, defaultModel string, vendor *testVendor) *PluginRegistry {
|
||||
t.Helper()
|
||||
db := fsdb.NewDb(t.TempDir())
|
||||
all := ai.NewVendorsManager()
|
||||
all.AddVendors(vendor)
|
||||
defaults := &tools.Defaults{
|
||||
PluginBase: &plugins.PluginBase{},
|
||||
Vendor: &plugins.Setting{Value: defaultVendor},
|
||||
Model: &plugins.SetupQuestion{Setting: &plugins.Setting{Value: defaultModel}},
|
||||
ModelContextLength: &plugins.SetupQuestion{Setting: &plugins.Setting{Value: "0"}},
|
||||
}
|
||||
return &PluginRegistry{
|
||||
Db: db,
|
||||
VendorManager: ai.NewVendorsManager(),
|
||||
VendorsAll: all,
|
||||
Defaults: defaults,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ func TestTranslation(t *testing.T) {
|
|||
{"pt", "usa a entrada original, porque não é possível aplicar a legibilidade HTML"},
|
||||
{"fa", "از ورودی اصلی استفاده کن، چون نمیتوان خوانایی HTML را اعمال کرد"},
|
||||
{"it", "usa l'input originale, perché non è possibile applicare la leggibilità HTML"},
|
||||
{"tr", "html okunabilirliği uygulanamadığından orijinal girdiyi kullanın"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "Wähle ein Muster aus den verfügbaren Mustern",
|
||||
"choose_session_from_available": "Wähle eine Sitzung aus den verfügbaren Sitzungen",
|
||||
"choose_strategy_from_available": "Strategie aus den verfügbaren Strategien wählen",
|
||||
"codex_api_base_url_question": "Geben Sie Ihre Codex-API-Basis-URL ein",
|
||||
"codex_auth_base_url_invalid": "Ungültige Codex-Authentifizierungs-Basis-URL: %w",
|
||||
"codex_auth_base_url_question": "Geben Sie Ihre Codex-OAuth-Basis-URL ein",
|
||||
"codex_browser_open_fallback": "Falls Ihr Browser sich nicht geöffnet hat, navigieren Sie zu dieser URL zur Authentifizierung:",
|
||||
"codex_decode_models_response_failed": "Codex-Modell-Antwort konnte nicht dekodiert werden: %w",
|
||||
"codex_decode_refresh_response_failed": "Aktualisierte Codex-Token-Antwort konnte nicht dekodiert werden: %w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "Sicherer zufälliger OAuth-Status konnte nicht generiert werden: %w",
|
||||
"codex_oauth_server_start_failed": "Lokaler OAuth-Callback-Server konnte nicht gestartet werden: %w",
|
||||
"codex_oauth_state_mismatch": "Status stimmt nicht überein",
|
||||
"codex_provider_error": "Codex-Anbieterfehler (Status %d): %s",
|
||||
"codex_refresh_failed_status": "Codex-Anmeldung konnte nicht aktualisiert werden (Status %d)",
|
||||
"codex_refresh_login_failed": "Codex-Anmeldung konnte nicht aktualisiert werden: %w",
|
||||
"codex_refresh_token_required": "Codex-Aktualisierungstoken ist erforderlich. Bitte führen Sie 'fabric --setup' erneut aus.",
|
||||
"codex_replay_body_unavailable": "Anfragekörper kann für Codex-Reauthentifizierungswiederholung nicht wiedergegeben werden",
|
||||
"codex_request_failed": "Codex-Anfrage fehlgeschlagen: %w",
|
||||
"codex_request_failed_status": "Codex-Anfrage fehlgeschlagen mit Status %d",
|
||||
"codex_starting_browser_login": "Starte browserbasierte OpenAI-Anmeldung für Codex.",
|
||||
"codex_token_exchange_failed": "Codex-Token-Austausch fehlgeschlagen: %w",
|
||||
"codex_token_persist_failed": "Codex-Anmeldung konnte nicht gespeichert werden: %w",
|
||||
"codex_token_refresh_missing_access_token": "Die Codex-Token-Aktualisierung hat kein Zugriffstoken zurückgegeben.",
|
||||
"codex_usage_limit_reached": "Codex-Nutzungslimit erreicht",
|
||||
"command_completed_successfully": "Befehl erfolgreich abgeschlossen",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "Benutzerdefinierte Patterns - Verzeichnis für Ihre benutzerdefinierten Patterns festlegen",
|
||||
"custom_patterns_warning_create_directory": "Warnung: Benutzerdefiniertes Musterverzeichnis %s konnte nicht erstellt werden: %v\n",
|
||||
"db_error_loading_env_file": "fehler beim Laden der .env-Datei: %w",
|
||||
"db_error_updating_env_file": "fehler beim Aktualisieren der .env-Datei: %w",
|
||||
"defaults_model_context_length_question": "Geben Sie die Kontextlänge des Modells ein",
|
||||
"defaults_model_question": "Geben Sie den Index oder den Namen Ihres Standardmodells ein",
|
||||
"defaults_setup_description": "Standard-KI-Anbieter und -Modell",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "DigitalOcean-Modellanfrage fehlgeschlagen mit Status %d: %s",
|
||||
"disable_openai_responses_api": "OpenAI Responses API deaktivieren (Standard: false)",
|
||||
"disable_pattern_variable_replacement": "Mustervariablenersetzung deaktivieren",
|
||||
"enable_web_search_tool": "Web-Such-Tool für unterstützte Modelle aktivieren (Anthropic, OpenAI, Gemini)",
|
||||
"enable_web_search_tool": "Web-Such-Tool für unterstützte Modelle aktivieren (Anthropic, OpenAI, Gemini, Grok)",
|
||||
"end_tag_thinking_sections": "End-Tag für Denk-Abschnitte",
|
||||
"error_creating_audio_file": "Fehler beim Erstellen der Audio-Datei: %v",
|
||||
"error_creating_file": "Fehler beim Erstellen der Datei: %v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "ungültiges Data-URL-Format",
|
||||
"ollama_invalid_http_timeout_using_default": "ungültiges HTTP-Zeitlimit '%s': %v, verwende Standardwert",
|
||||
"ollama_invalid_num_ctx_in_request": "Ungültiger num_ctx in Anfrage: %v",
|
||||
"ollama_invalid_request_body": "ungültiger Anfragetext",
|
||||
"ollama_no_content_from_upstream": "Kein Inhalt vom Upstream Fabric Server erhalten",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx überschreitet den maximal zulässigen Wert von %d",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx muss eine Zahl sein, ungültiger Typ erhalten",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "SSE Zeile überschreitet 1MB Puffer-Limit - Datenzeile zu groß",
|
||||
"ollama_upstream_non_2xx": "Upstream Fabric Server hat nicht-2xx Status %d zurückgegeben: %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "Upstream Fabric Server hat nicht-2xx Status %d zurückgegeben und Body konnte nicht gelesen werden: %v",
|
||||
"ollama_upstream_request_failed": "Upstream-Fabric-Server nicht erreichbar",
|
||||
"ollama_upstream_returned_status": "Upstream Fabric Server hat Status %d zurückgegeben",
|
||||
"ollama_warning_no_content": "Warnung: Kein Inhalt vom Upstream Fabric Server erhalten",
|
||||
"ollama_warning_parse_variables": "Warnung: Fehler beim Parsen von options.variables als JSON: %v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "Playlist gegenüber Video bevorzugen, wenn beide IDs in der URL vorhanden sind",
|
||||
"print_context": "Kontext ausgeben",
|
||||
"print_current_version": "Aktuelle Version ausgeben",
|
||||
"print_metadata_to_stderr": "Metadaten (Eingabe-/Ausgabe-Token) auf stderr ausgeben",
|
||||
"print_pattern_contents": "Den Inhalt des angegebenen Musters im Terminal ausgeben",
|
||||
"print_session": "Sitzung ausgeben",
|
||||
"register_new_extension": "Neue Erweiterung aus Konfigurationsdateipfad registrieren",
|
||||
"remove_registered_extension": "Registrierte Erweiterung nach Name entfernen",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "Desktop-Benachrichtigung senden, wenn Befehl abgeschlossen ist",
|
||||
"serve_fabric_api_ollama_endpoints": "Fabric REST API mit ollama-Endpunkten bereitstellen",
|
||||
"serve_fabric_rest_api": "Fabric REST API bereitstellen",
|
||||
"server_api_key_required": "Server-Start auf Nicht-Loopback-Adresse %s ohne API-Schlüssel verweigert: Setzen Sie --api-key oder FABRIC_API_KEY, oder binden Sie eine Loopback-Adresse wie 127.0.0.1:8080",
|
||||
"server_chat_error": "Fehler: %v",
|
||||
"server_error_marshaling_response": "Fehler beim Serialisieren der Antwort: %v",
|
||||
"server_error_writing_response": "Fehler beim Schreiben der Antwort: %v",
|
||||
"server_invalid_request_format": "ungültiges Anfrageformat: %v",
|
||||
"server_no_api_key_warning": "REST-API-Server wird ohne API-Schlüssel-Authentifizierung gestartet. Dies kann Sicherheitsrisiken bergen.",
|
||||
"sessions_creating_new": "Erstelle neue Sitzung: %s\n",
|
||||
"set_debug_level": "Debug-Level festlegen (0=aus, 1=grundlegend, 2=detailliert, 3=Trace, 4=wire)",
|
||||
"set_frequency_penalty": "Häufigkeitsstrafe festlegen",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**Sendung**: %s",
|
||||
"spotify_title_label": "**Titel**: %s",
|
||||
"spotify_total_episodes_label": "**Episoden insgesamt**: %d",
|
||||
"spotify_url_help": "Spotify-Podcast- oder Episoden-URL, um Metadaten abzurufen und an den Chat zu senden",
|
||||
"spotify_url_label": "**URL**: %s",
|
||||
"start_tag_thinking_sections": "Start-Tag für Denk-Abschnitte",
|
||||
"storage_error_delete": "%s konnte nicht gelöscht werden: %v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "%s konnte nicht gespeichert werden: %v",
|
||||
"storage_error_stat_entry": "Eintrag %s konnte nicht abgefragt werden: %v",
|
||||
"storage_error_unmarshal": "%s konnte nicht deserialisiert werden: %s",
|
||||
"storage_invalid_name": "Ungültiger Name: %q",
|
||||
"strategies_available_header": "Verfügbare Strategien:",
|
||||
"strategies_cloning_repository": "Repository %s wird geklont (Pfad: %s)...\\n",
|
||||
"strategies_download_success": "✅ Strategien erfolgreich nach %s heruntergeladen und installiert\\n",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "Choose a pattern from the available patterns",
|
||||
"choose_session_from_available": "Choose a session from the available sessions",
|
||||
"choose_strategy_from_available": "Choose a strategy from the available strategies",
|
||||
"codex_api_base_url_question": "Enter your Codex API base URL",
|
||||
"codex_auth_base_url_invalid": "invalid codex auth base url: %w",
|
||||
"codex_auth_base_url_question": "Enter your Codex OAuth base URL",
|
||||
"codex_browser_open_fallback": "If your browser did not open, navigate to this URL to authenticate:",
|
||||
"codex_decode_models_response_failed": "failed to decode codex models response: %w",
|
||||
"codex_decode_refresh_response_failed": "failed to decode refreshed Codex token response: %w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "failed to generate secure random oauth state: %w",
|
||||
"codex_oauth_server_start_failed": "failed to start local oauth callback server: %w",
|
||||
"codex_oauth_state_mismatch": "State mismatch",
|
||||
"codex_provider_error": "codex provider error (status %d): %s",
|
||||
"codex_refresh_failed_status": "failed to refresh codex login (status %d)",
|
||||
"codex_refresh_login_failed": "failed to refresh Codex login: %w",
|
||||
"codex_refresh_token_required": "Codex refresh token is required. Please rerun 'fabric --setup'.",
|
||||
"codex_replay_body_unavailable": "request body cannot be replayed for Codex re-authentication retry",
|
||||
"codex_request_failed": "codex request failed: %w",
|
||||
"codex_request_failed_status": "codex request failed with status %d",
|
||||
"codex_starting_browser_login": "Starting browser-based OpenAI login for Codex.",
|
||||
"codex_token_exchange_failed": "codex token exchange failed: %w",
|
||||
"codex_token_persist_failed": "failed to persist Codex login: %w",
|
||||
"codex_token_refresh_missing_access_token": "Codex token refresh did not return an access token.",
|
||||
"codex_usage_limit_reached": "codex usage limit reached",
|
||||
"command_completed_successfully": "Command completed successfully",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "Custom Patterns - Set directory for your custom patterns",
|
||||
"custom_patterns_warning_create_directory": "Warning: Could not create custom patterns directory %s: %v\n",
|
||||
"db_error_loading_env_file": "error loading .env file: %w",
|
||||
"db_error_updating_env_file": "error updating .env file: %w",
|
||||
"defaults_model_context_length_question": "Enter model context length",
|
||||
"defaults_model_question": "Enter the index or the name of your default model",
|
||||
"defaults_setup_description": "Default AI Vendor and Model",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "DigitalOcean models request failed with status %d: %s",
|
||||
"disable_openai_responses_api": "Disable OpenAI Responses API (default: false)",
|
||||
"disable_pattern_variable_replacement": "Disable pattern variable replacement",
|
||||
"enable_web_search_tool": "Enable web search tool for supported models (Anthropic, OpenAI, Gemini)",
|
||||
"enable_web_search_tool": "Enable web search tool for supported models (Anthropic, OpenAI, Gemini, Grok)",
|
||||
"end_tag_thinking_sections": "End tag for thinking sections",
|
||||
"error_creating_audio_file": "error creating audio file: %v",
|
||||
"error_creating_file": "error creating file: %v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "invalid data URL format",
|
||||
"ollama_invalid_http_timeout_using_default": "invalid HTTP timeout '%s': %v, using default",
|
||||
"ollama_invalid_num_ctx_in_request": "invalid num_ctx in request: %v",
|
||||
"ollama_invalid_request_body": "invalid request body",
|
||||
"ollama_no_content_from_upstream": "no content received from upstream Fabric server",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx exceeds maximum allowed value of %d",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx must be a number, got invalid type",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "SSE line exceeds 1MB buffer limit - data line too large",
|
||||
"ollama_upstream_non_2xx": "Upstream Fabric server returned non-2xx status %d: %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "Upstream Fabric server returned non-2xx status %d and body could not be read: %v",
|
||||
"ollama_upstream_request_failed": "failed to reach upstream Fabric server",
|
||||
"ollama_upstream_returned_status": "upstream Fabric server returned status %d",
|
||||
"ollama_warning_no_content": "Warning: no content received from upstream Fabric server",
|
||||
"ollama_warning_parse_variables": "Warning: failed to parse options.variables as JSON: %v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "Prefer playlist over video if both ids are present in the URL",
|
||||
"print_context": "Print context",
|
||||
"print_current_version": "Print current version",
|
||||
"print_metadata_to_stderr": "Print metadata (input/output tokens) to stderr",
|
||||
"print_pattern_contents": "Print the contents of the named pattern to the terminal",
|
||||
"print_session": "Print session",
|
||||
"register_new_extension": "Register a new extension from config file path",
|
||||
"remove_registered_extension": "Remove a registered extension by name",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "Send desktop notification when command completes",
|
||||
"serve_fabric_api_ollama_endpoints": "Serve the Fabric Rest API with ollama endpoints",
|
||||
"serve_fabric_rest_api": "Serve the Fabric Rest API",
|
||||
"server_api_key_required": "refusing to serve on non-loopback address %s without an API key: set --api-key or FABRIC_API_KEY, or bind a loopback address such as 127.0.0.1:8080",
|
||||
"server_chat_error": "Error: %v",
|
||||
"server_error_marshaling_response": "error marshaling response: %v",
|
||||
"server_error_writing_response": "error writing response: %v",
|
||||
"server_invalid_request_format": "invalid request format: %v",
|
||||
"server_no_api_key_warning": "Starting REST API server without API key authentication. This may pose security risks.",
|
||||
"sessions_creating_new": "Creating new session: %s\n",
|
||||
"set_debug_level": "Set debug level (0=off, 1=basic, 2=detailed, 3=trace, 4=wire)",
|
||||
"set_frequency_penalty": "Set frequency penalty",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**Show**: %s",
|
||||
"spotify_title_label": "**Title**: %s",
|
||||
"spotify_total_episodes_label": "**Total Episodes**: %d",
|
||||
"spotify_url_help": "Spotify podcast or episode URL to grab metadata from and send to chat",
|
||||
"spotify_url_label": "**URL**: %s",
|
||||
"start_tag_thinking_sections": "Start tag for thinking sections",
|
||||
"storage_error_delete": "could not delete %s: %v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "could not save %s: %v",
|
||||
"storage_error_stat_entry": "could not stat entry %s: %v",
|
||||
"storage_error_unmarshal": "could not unmarshal %s: %s",
|
||||
"storage_invalid_name": "invalid name: %q",
|
||||
"strategies_available_header": "Available Strategies:",
|
||||
"strategies_cloning_repository": "Cloning repository %s (path: %s)...\n",
|
||||
"strategies_download_success": "✅ Successfully downloaded and installed strategies to %s\n",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "Elige un patrón de los patrones disponibles",
|
||||
"choose_session_from_available": "Elige una sesión de las sesiones disponibles",
|
||||
"choose_strategy_from_available": "Elegir una estrategia de las estrategias disponibles",
|
||||
"codex_api_base_url_question": "Ingrese la URL base de la API de Codex",
|
||||
"codex_auth_base_url_invalid": "URL base de autenticación de Codex no válida: %w",
|
||||
"codex_auth_base_url_question": "Ingrese la URL base de OAuth de Codex",
|
||||
"codex_browser_open_fallback": "Si su navegador no se abrió, navegue a esta URL para autenticarse:",
|
||||
"codex_decode_models_response_failed": "No se pudo decodificar la respuesta de modelos de Codex: %w",
|
||||
"codex_decode_refresh_response_failed": "No se pudo decodificar la respuesta de token actualizado de Codex: %w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "No se pudo generar un estado OAuth aleatorio seguro: %w",
|
||||
"codex_oauth_server_start_failed": "No se pudo iniciar el servidor local de callback OAuth: %w",
|
||||
"codex_oauth_state_mismatch": "El estado no coincide",
|
||||
"codex_provider_error": "error del proveedor de Codex (estado %d): %s",
|
||||
"codex_refresh_failed_status": "No se pudo actualizar el inicio de sesión de Codex (estado %d)",
|
||||
"codex_refresh_login_failed": "No se pudo actualizar el inicio de sesión de Codex: %w",
|
||||
"codex_refresh_token_required": "Se requiere el token de actualización de Codex. Ejecute 'fabric --setup' de nuevo.",
|
||||
"codex_replay_body_unavailable": "El cuerpo de la solicitud no se puede reproducir para el reintento de reautenticación de Codex",
|
||||
"codex_request_failed": "La solicitud de Codex falló: %w",
|
||||
"codex_request_failed_status": "La solicitud de Codex falló con estado %d",
|
||||
"codex_starting_browser_login": "Iniciando inicio de sesión de OpenAI basado en navegador para Codex.",
|
||||
"codex_token_exchange_failed": "El intercambio de token de Codex falló: %w",
|
||||
"codex_token_persist_failed": "no se pudo guardar el inicio de sesión de Codex: %w",
|
||||
"codex_token_refresh_missing_access_token": "La actualización del token de Codex no devolvió un token de acceso.",
|
||||
"codex_usage_limit_reached": "Límite de uso de Codex alcanzado",
|
||||
"command_completed_successfully": "Comando completado exitosamente",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "Patrones personalizados - Establecer directorio para tus patrones personalizados",
|
||||
"custom_patterns_warning_create_directory": "Advertencia: No se pudo crear el directorio de patrones personalizados %s: %v\n",
|
||||
"db_error_loading_env_file": "error al cargar el archivo .env: %w",
|
||||
"db_error_updating_env_file": "error al actualizar el archivo .env: %w",
|
||||
"defaults_model_context_length_question": "Introduce la longitud del contexto del modelo",
|
||||
"defaults_model_question": "Introduce el índice o el nombre de tu modelo predeterminado",
|
||||
"defaults_setup_description": "Proveedor y modelo de IA predeterminados",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "solicitud de modelos de DigitalOcean falló con estado %d: %s",
|
||||
"disable_openai_responses_api": "Deshabilitar API de Respuestas de OpenAI (predeterminado: false)",
|
||||
"disable_pattern_variable_replacement": "Deshabilitar reemplazo de variables de patrón",
|
||||
"enable_web_search_tool": "Habilitar herramienta de búsqueda web para modelos soportados (Anthropic, OpenAI, Gemini)",
|
||||
"enable_web_search_tool": "Habilitar herramienta de búsqueda web para modelos soportados (Anthropic, OpenAI, Gemini, Grok)",
|
||||
"end_tag_thinking_sections": "Etiqueta de fin para secciones de pensamiento",
|
||||
"error_creating_audio_file": "error al crear el archivo de audio: %v",
|
||||
"error_creating_file": "error al crear el archivo: %v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "formato de URL de datos inválido",
|
||||
"ollama_invalid_http_timeout_using_default": "Tiempo de espera HTTP inválido '%s': %v, usando el valor predeterminado",
|
||||
"ollama_invalid_num_ctx_in_request": "num_ctx inválido en la solicitud: %v",
|
||||
"ollama_invalid_request_body": "cuerpo de solicitud no válido",
|
||||
"ollama_no_content_from_upstream": "no se recibió contenido del servidor Fabric upstream",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx excede el valor máximo permitido de %d",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx debe ser un número, se obtuvo tipo inválido",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "Línea SSE excede el límite de búfer de 1MB - línea de datos demasiado grande",
|
||||
"ollama_upstream_non_2xx": "El servidor Fabric upstream devolvió estado no-2xx %d: %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "El servidor Fabric upstream devolvió estado no-2xx %d y el cuerpo no se pudo leer: %v",
|
||||
"ollama_upstream_request_failed": "no se pudo conectar con el servidor Fabric ascendente",
|
||||
"ollama_upstream_returned_status": "el servidor Fabric upstream devolvió estado %d",
|
||||
"ollama_warning_no_content": "Advertencia: no se recibió contenido del servidor Fabric upstream",
|
||||
"ollama_warning_parse_variables": "Advertencia: error al analizar options.variables como JSON: %v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "Preferir lista de reproducción sobre video si ambos ids están presentes en la URL",
|
||||
"print_context": "Imprimir contexto",
|
||||
"print_current_version": "Imprimir versión actual",
|
||||
"print_metadata_to_stderr": "Imprimir metadatos (tokens de entrada/salida) en stderr",
|
||||
"print_pattern_contents": "Imprimir el contenido del patrón indicado en la terminal",
|
||||
"print_session": "Imprimir sesión",
|
||||
"register_new_extension": "Registrar una nueva extensión desde la ruta del archivo de configuración",
|
||||
"remove_registered_extension": "Eliminar una extensión registrada por nombre",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "Enviar notificación de escritorio cuando se complete el comando",
|
||||
"serve_fabric_api_ollama_endpoints": "Servir la API REST de Fabric con endpoints de ollama",
|
||||
"serve_fabric_rest_api": "Servir la API REST de Fabric",
|
||||
"server_api_key_required": "se rechaza servir en la dirección no loopback %s sin clave de API: configure --api-key o FABRIC_API_KEY, o use una dirección loopback como 127.0.0.1:8080",
|
||||
"server_chat_error": "Error: %v",
|
||||
"server_error_marshaling_response": "error al serializar la respuesta: %v",
|
||||
"server_error_writing_response": "error al escribir la respuesta: %v",
|
||||
"server_invalid_request_format": "formato de solicitud no válido: %v",
|
||||
"server_no_api_key_warning": "Iniciando el servidor de API REST sin autenticación por clave de API. Esto puede suponer riesgos de seguridad.",
|
||||
"sessions_creating_new": "Creando nueva sesión: %s\n",
|
||||
"set_debug_level": "Establecer nivel de depuración (0=apagado, 1=básico, 2=detallado, 3=rastreo, 4=wire)",
|
||||
"set_frequency_penalty": "Establecer penalización de frecuencia",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**Programa**: %s",
|
||||
"spotify_title_label": "**Título**: %s",
|
||||
"spotify_total_episodes_label": "**Total de episodios**: %d",
|
||||
"spotify_url_help": "URL de podcast o episodio de Spotify para obtener metadatos y enviarlos al chat",
|
||||
"spotify_url_label": "**URL**: %s",
|
||||
"start_tag_thinking_sections": "Etiqueta de inicio para secciones de pensamiento",
|
||||
"storage_error_delete": "No se pudo eliminar %s: %v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "No se pudo guardar %s: %v",
|
||||
"storage_error_stat_entry": "No se pudo obtener información de la entrada %s: %v",
|
||||
"storage_error_unmarshal": "No se pudo deserializar %s: %s",
|
||||
"storage_invalid_name": "nombre inválido: %q",
|
||||
"strategies_available_header": "Estrategias disponibles:",
|
||||
"strategies_cloning_repository": "Clonando el repositorio %s (ruta: %s)...\\n",
|
||||
"strategies_download_success": "✅ Estrategias descargadas e instaladas correctamente en %s\\n",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "الگویی از الگوهای موجود انتخاب کنید",
|
||||
"choose_session_from_available": "جلسهای از جلسات موجود انتخاب کنید",
|
||||
"choose_strategy_from_available": "انتخاب استراتژی از استراتژیهای موجود",
|
||||
"codex_api_base_url_question": "آدرس پایه API Codex را وارد کنید",
|
||||
"codex_auth_base_url_invalid": "آدرس پایه احراز هویت Codex نامعتبر است: %w",
|
||||
"codex_auth_base_url_question": "آدرس پایه OAuth Codex را وارد کنید",
|
||||
"codex_browser_open_fallback": "اگر مرورگر شما باز نشد، برای احراز هویت به این آدرس بروید:",
|
||||
"codex_decode_models_response_failed": "رمزگشایی پاسخ مدلهای Codex ناموفق بود: %w",
|
||||
"codex_decode_refresh_response_failed": "رمزگشایی پاسخ توکن بازنشانیشده Codex ناموفق بود: %w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "تولید وضعیت تصادفی امن OAuth ناموفق بود: %w",
|
||||
"codex_oauth_server_start_failed": "راهاندازی سرور محلی بازگشت OAuth ناموفق بود: %w",
|
||||
"codex_oauth_state_mismatch": "وضعیت مطابقت ندارد",
|
||||
"codex_provider_error": "خطای ارائهدهنده Codex (وضعیت %d): %s",
|
||||
"codex_refresh_failed_status": "بازنشانی ورود Codex ناموفق بود (وضعیت %d)",
|
||||
"codex_refresh_login_failed": "بازنشانی ورود Codex ناموفق بود: %w",
|
||||
"codex_refresh_token_required": "توکن بازنشانی Codex مورد نیاز است. لطفاً 'fabric --setup' را دوباره اجرا کنید.",
|
||||
"codex_replay_body_unavailable": "بدنه درخواست برای تلاش مجدد احراز هویت Codex قابل بازپخش نیست",
|
||||
"codex_request_failed": "درخواست Codex ناموفق بود: %w",
|
||||
"codex_request_failed_status": "درخواست Codex با وضعیت %d ناموفق بود",
|
||||
"codex_starting_browser_login": "شروع ورود مبتنی بر مرورگر OpenAI برای Codex.",
|
||||
"codex_token_exchange_failed": "تبادل توکن Codex ناموفق بود: %w",
|
||||
"codex_token_persist_failed": "ذخیره ورود Codex ناموفق بود: %w",
|
||||
"codex_token_refresh_missing_access_token": "بازنشانی توکن Codex توکن دسترسی را برنگرداند.",
|
||||
"codex_usage_limit_reached": "محدودیت استفاده Codex به حداکثر رسیده است",
|
||||
"command_completed_successfully": "دستور با موفقیت تکمیل شد",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "الگوهای سفارشی - تنظیم دایرکتوری برای الگوهای سفارشی شما",
|
||||
"custom_patterns_warning_create_directory": "هشدار: امکان ایجاد پوشه الگوهای سفارشی %s وجود ندارد: %v\n",
|
||||
"db_error_loading_env_file": "خطا در بارگذاری فایل .env: %w",
|
||||
"db_error_updating_env_file": "خطا در بهروزرسانی فایل .env: %w",
|
||||
"defaults_model_context_length_question": "طول زمینه مدل را وارد کنید",
|
||||
"defaults_model_question": "شاخص یا نام مدل پیشفرض خود را وارد کنید",
|
||||
"defaults_setup_description": "ارائهدهنده و مدل هوش مصنوعی پیشفرض",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "درخواست مدلهای DigitalOcean با وضعیت %d ناموفق بود: %s",
|
||||
"disable_openai_responses_api": "غیرفعال کردن API OpenAI Responses (پیشفرض: false)",
|
||||
"disable_pattern_variable_replacement": "غیرفعال کردن جایگزینی متغیرهای الگو",
|
||||
"enable_web_search_tool": "فعالسازی ابزار جستجوی وب برای مدلهای پشتیبانی شده (Anthropic، OpenAI، Gemini)",
|
||||
"enable_web_search_tool": "فعالسازی ابزار جستجوی وب برای مدلهای پشتیبانی شده (Anthropic، OpenAI، Gemini، Grok)",
|
||||
"end_tag_thinking_sections": "تگ پایان برای بخشهای تفکر",
|
||||
"error_creating_audio_file": "خطا در ایجاد فایل صوتی: %v",
|
||||
"error_creating_file": "خطا در ایجاد فایل: %v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "فرمت URL داده نامعتبر",
|
||||
"ollama_invalid_http_timeout_using_default": "زمان انتظار HTTP نامعتبر '%s': %v، استفاده از مقدار پیشفرض",
|
||||
"ollama_invalid_num_ctx_in_request": "num_ctx نامعتبر در درخواست: %v",
|
||||
"ollama_invalid_request_body": "بدنه درخواست نامعتبر",
|
||||
"ollama_no_content_from_upstream": "هیچ محتوایی از سرور Fabric بالادستی دریافت نشد",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx از حداکثر مقدار مجاز %d فراتر رفته است",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx باید یک عدد باشد، نوع نامعتبر دریافت شد",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "خط SSE از حد بافر 1MB فراتر رفت - خط داده بیش از حد بزرگ است",
|
||||
"ollama_upstream_non_2xx": "سرور Fabric بالادستی وضعیت غیر-2xx %d را برگرداند: %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "سرور Fabric بالادستی وضعیت غیر-2xx %d را برگرداند و بدنه قابل خواندن نبود: %v",
|
||||
"ollama_upstream_request_failed": "دسترسی به سرور بالادستی Fabric ممکن نیست",
|
||||
"ollama_upstream_returned_status": "سرور Fabric بالادستی وضعیت %d را برگرداند",
|
||||
"ollama_warning_no_content": "هشدار: هیچ محتوایی از سرور Fabric بالادستی دریافت نشد",
|
||||
"ollama_warning_parse_variables": "هشدار: شکست در تجزیه options.variables به عنوان JSON: %v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "اولویت فهرست پخش نسبت به ویدیو اگر هر دو ID در URL موجود باشند",
|
||||
"print_context": "چاپ زمینه",
|
||||
"print_current_version": "چاپ نسخه فعلی",
|
||||
"print_metadata_to_stderr": "چاپ فراداده (توکنهای ورودی/خروجی) در stderr",
|
||||
"print_pattern_contents": "چاپ محتوای الگوی مشخصشده در ترمینال",
|
||||
"print_session": "چاپ جلسه",
|
||||
"register_new_extension": "ثبت افزونه جدید از مسیر فایل پیکربندی",
|
||||
"remove_registered_extension": "حذف افزونه ثبت شده با نام",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "ارسال اعلان دسکتاپ هنگام تکمیل دستور",
|
||||
"serve_fabric_api_ollama_endpoints": "سرویس API REST Fabric با نقاط پایانی ollama",
|
||||
"serve_fabric_rest_api": "سرویس API REST Fabric",
|
||||
"server_api_key_required": "سرویسدهی روی آدرس غیر loopback %s بدون کلید API رد شد: --api-key یا FABRIC_API_KEY را تنظیم کنید، یا به یک آدرس loopback مانند 127.0.0.1:8080 متصل شوید",
|
||||
"server_chat_error": "خطا: %v",
|
||||
"server_error_marshaling_response": "خطا در سریالسازی پاسخ: %v",
|
||||
"server_error_writing_response": "خطا در نوشتن پاسخ: %v",
|
||||
"server_invalid_request_format": "فرمت درخواست نامعتبر: %v",
|
||||
"server_no_api_key_warning": "سرور REST API بدون احراز هویت کلید API راهاندازی میشود. این ممکن است خطرات امنیتی ایجاد کند.",
|
||||
"sessions_creating_new": "ایجاد نشست جدید: %s\n",
|
||||
"set_debug_level": "تنظیم سطح اشکالزدایی (0=خاموش، 1=پایه، 2=تفصیلی، 3=ردیابی، 4=wire)",
|
||||
"set_frequency_penalty": "تنظیم جریمه فرکانس",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**برنامه**: %s",
|
||||
"spotify_title_label": "**عنوان**: %s",
|
||||
"spotify_total_episodes_label": "**مجموع اپیزودها**: %d",
|
||||
"spotify_url_help": "نشانی پادکست یا قسمت Spotify برای دریافت فراداده و ارسال به گفتگو",
|
||||
"spotify_url_label": "**URL**: %s",
|
||||
"start_tag_thinking_sections": "تگ شروع برای بخشهای تفکر",
|
||||
"storage_error_delete": "حذف %s ناموفق بود: %v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "ذخیره %s ناموفق بود: %v",
|
||||
"storage_error_stat_entry": "دریافت اطلاعات ورودی %s ناموفق بود: %v",
|
||||
"storage_error_unmarshal": "بازسریالسازی %s ناموفق بود: %s",
|
||||
"storage_invalid_name": "نام نامعتبر: %q",
|
||||
"strategies_available_header": "راهبردهای موجود:",
|
||||
"strategies_cloning_repository": "در حال کلون کردن مخزن %s (مسیر: %s)...\\n",
|
||||
"strategies_download_success": "✅ راهبردها با موفقیت در %s دانلود و نصب شدند\\n",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "Choisissez un motif parmi les motifs disponibles",
|
||||
"choose_session_from_available": "Choisissez une session parmi les sessions disponibles",
|
||||
"choose_strategy_from_available": "Choisir une stratégie parmi les stratégies disponibles",
|
||||
"codex_api_base_url_question": "Entrez l'URL de base de l'API Codex",
|
||||
"codex_auth_base_url_invalid": "URL de base d'authentification Codex invalide : %w",
|
||||
"codex_auth_base_url_question": "Entrez l'URL de base OAuth de Codex",
|
||||
"codex_browser_open_fallback": "Si votre navigateur ne s'est pas ouvert, accédez à cette URL pour vous authentifier :",
|
||||
"codex_decode_models_response_failed": "Échec du décodage de la réponse des modèles Codex : %w",
|
||||
"codex_decode_refresh_response_failed": "Échec du décodage de la réponse du jeton Codex rafraîchi : %w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "Échec de la génération d'un état OAuth aléatoire sécurisé : %w",
|
||||
"codex_oauth_server_start_failed": "Échec du démarrage du serveur local de rappel OAuth : %w",
|
||||
"codex_oauth_state_mismatch": "L'état ne correspond pas",
|
||||
"codex_provider_error": "erreur du fournisseur Codex (statut %d) : %s",
|
||||
"codex_refresh_failed_status": "Échec du rafraîchissement de la connexion Codex (statut %d)",
|
||||
"codex_refresh_login_failed": "Échec du rafraîchissement de la connexion Codex : %w",
|
||||
"codex_refresh_token_required": "Le jeton de rafraîchissement Codex est requis. Veuillez relancer 'fabric --setup'.",
|
||||
"codex_replay_body_unavailable": "Le corps de la requête ne peut pas être rejoué pour la tentative de réauthentification Codex",
|
||||
"codex_request_failed": "La requête Codex a échoué : %w",
|
||||
"codex_request_failed_status": "La requête Codex a échoué avec le statut %d",
|
||||
"codex_starting_browser_login": "Démarrage de la connexion OpenAI par navigateur pour Codex.",
|
||||
"codex_token_exchange_failed": "L'échange de jeton Codex a échoué : %w",
|
||||
"codex_token_persist_failed": "échec de l'enregistrement de la connexion Codex : %w",
|
||||
"codex_token_refresh_missing_access_token": "Le rafraîchissement du jeton Codex n'a pas renvoyé de jeton d'accès.",
|
||||
"codex_usage_limit_reached": "Limite d'utilisation Codex atteinte",
|
||||
"command_completed_successfully": "Commande terminée avec succès",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "Patrons personnalisés - Définir le répertoire pour vos patrons personnalisés",
|
||||
"custom_patterns_warning_create_directory": "Avertissement : Impossible de créer le répertoire de modèles personnalisés %s : %v\n",
|
||||
"db_error_loading_env_file": "erreur lors du chargement du fichier .env : %w",
|
||||
"db_error_updating_env_file": "erreur lors de la mise à jour du fichier .env : %w",
|
||||
"defaults_model_context_length_question": "Saisissez la longueur du contexte du modèle",
|
||||
"defaults_model_question": "Saisissez l'index ou le nom de votre modèle par défaut",
|
||||
"defaults_setup_description": "Fournisseur et modèle d'IA par défaut",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "échec de la requête de modèles DigitalOcean avec le statut %d : %s",
|
||||
"disable_openai_responses_api": "Désactiver l'API OpenAI Responses (par défaut : false)",
|
||||
"disable_pattern_variable_replacement": "Désactiver le remplacement des variables de motif",
|
||||
"enable_web_search_tool": "Activer l'outil de recherche web pour les modèles pris en charge (Anthropic, OpenAI, Gemini)",
|
||||
"enable_web_search_tool": "Activer l'outil de recherche web pour les modèles pris en charge (Anthropic, OpenAI, Gemini, Grok)",
|
||||
"end_tag_thinking_sections": "Balise de fin pour les sections de réflexion",
|
||||
"error_creating_audio_file": "erreur lors de la création du fichier audio : %v",
|
||||
"error_creating_file": "erreur lors de la création du fichier : %v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "format d'URL de données invalide",
|
||||
"ollama_invalid_http_timeout_using_default": "Délai d'expiration HTTP invalide '%s' : %v, utilisation de la valeur par défaut",
|
||||
"ollama_invalid_num_ctx_in_request": "num_ctx invalide dans la requête : %v",
|
||||
"ollama_invalid_request_body": "corps de requête invalide",
|
||||
"ollama_no_content_from_upstream": "aucun contenu reçu du serveur Fabric en amont",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx dépasse la valeur maximale autorisée de %d",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx doit être un nombre, type invalide reçu",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "La ligne SSE dépasse la limite de tampon de 1 Mo - ligne de données trop grande",
|
||||
"ollama_upstream_non_2xx": "Le serveur Fabric en amont a renvoyé un statut non-2xx %d : %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "Le serveur Fabric en amont a renvoyé un statut non-2xx %d et le corps n'a pas pu être lu : %v",
|
||||
"ollama_upstream_request_failed": "impossible de joindre le serveur Fabric en amont",
|
||||
"ollama_upstream_returned_status": "le serveur Fabric en amont a renvoyé le statut %d",
|
||||
"ollama_warning_no_content": "Attention : aucun contenu reçu du serveur Fabric en amont",
|
||||
"ollama_warning_parse_variables": "Attention : échec de l'analyse de options.variables en JSON : %v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "Préférer la liste de lecture à la vidéo si les deux IDs sont présents dans l'URL",
|
||||
"print_context": "Afficher le contexte",
|
||||
"print_current_version": "Afficher la version actuelle",
|
||||
"print_metadata_to_stderr": "Afficher les métadonnées (jetons d'entrée/sortie) sur stderr",
|
||||
"print_pattern_contents": "Afficher le contenu du motif indiqué dans le terminal",
|
||||
"print_session": "Afficher la session",
|
||||
"register_new_extension": "Enregistrer une nouvelle extension depuis le chemin du fichier de configuration",
|
||||
"remove_registered_extension": "Supprimer une extension enregistrée par nom",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "Envoyer une notification de bureau quand la commande se termine",
|
||||
"serve_fabric_api_ollama_endpoints": "Servir l'API REST Fabric avec les endpoints ollama",
|
||||
"serve_fabric_rest_api": "Servir l'API REST Fabric",
|
||||
"server_api_key_required": "refus de servir sur l'adresse non loopback %s sans clé API : définissez --api-key ou FABRIC_API_KEY, ou liez une adresse loopback comme 127.0.0.1:8080",
|
||||
"server_chat_error": "Erreur : %v",
|
||||
"server_error_marshaling_response": "erreur de sérialisation de la réponse : %v",
|
||||
"server_error_writing_response": "erreur d'écriture de la réponse : %v",
|
||||
"server_invalid_request_format": "format de requête invalide : %v",
|
||||
"server_no_api_key_warning": "Démarrage du serveur API REST sans authentification par clé API. Cela peut présenter des risques de sécurité.",
|
||||
"sessions_creating_new": "Création d'une nouvelle session : %s\n",
|
||||
"set_debug_level": "Définir le niveau de débogage (0=désactivé, 1=basique, 2=détaillé, 3=trace, 4=wire)",
|
||||
"set_frequency_penalty": "Définir la pénalité de fréquence",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**Émission** : %s",
|
||||
"spotify_title_label": "**Titre** : %s",
|
||||
"spotify_total_episodes_label": "**Épisodes au total** : %d",
|
||||
"spotify_url_help": "URL de podcast ou d'épisode Spotify pour récupérer les métadonnées et les envoyer au chat",
|
||||
"spotify_url_label": "**URL** : %s",
|
||||
"start_tag_thinking_sections": "Balise de début pour les sections de réflexion",
|
||||
"storage_error_delete": "Impossible de supprimer %s : %v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "Impossible de sauvegarder %s : %v",
|
||||
"storage_error_stat_entry": "Impossible d'obtenir les informations de l'entrée %s : %v",
|
||||
"storage_error_unmarshal": "Impossible de désérialiser %s : %s",
|
||||
"storage_invalid_name": "nom invalide : %q",
|
||||
"strategies_available_header": "Stratégies disponibles :",
|
||||
"strategies_cloning_repository": "Clonage du dépôt %s (chemin : %s)...\\n",
|
||||
"strategies_download_success": "✅ Stratégies téléchargées et installées avec succès dans %s\\n",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "Scegli un pattern dai pattern disponibili",
|
||||
"choose_session_from_available": "Scegli una sessione dalle sessioni disponibili",
|
||||
"choose_strategy_from_available": "Scegli una strategia dalle strategie disponibili",
|
||||
"codex_api_base_url_question": "Inserisci l'URL di base dell'API Codex",
|
||||
"codex_auth_base_url_invalid": "URL base di autenticazione Codex non valido: %w",
|
||||
"codex_auth_base_url_question": "Inserisci l'URL di base OAuth di Codex",
|
||||
"codex_browser_open_fallback": "Se il browser non si è aperto, navigare a questo URL per autenticarsi:",
|
||||
"codex_decode_models_response_failed": "Decodifica della risposta dei modelli Codex non riuscita: %w",
|
||||
"codex_decode_refresh_response_failed": "Decodifica della risposta del token Codex aggiornato non riuscita: %w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "Generazione dello stato OAuth casuale sicuro non riuscita: %w",
|
||||
"codex_oauth_server_start_failed": "Avvio del server locale di callback OAuth non riuscito: %w",
|
||||
"codex_oauth_state_mismatch": "Lo stato non corrisponde",
|
||||
"codex_provider_error": "errore del fornitore Codex (stato %d): %s",
|
||||
"codex_refresh_failed_status": "Aggiornamento dell'accesso Codex non riuscito (stato %d)",
|
||||
"codex_refresh_login_failed": "Aggiornamento dell'accesso Codex non riuscito: %w",
|
||||
"codex_refresh_token_required": "Il token di aggiornamento Codex è richiesto. Eseguire di nuovo 'fabric --setup'.",
|
||||
"codex_replay_body_unavailable": "Il corpo della richiesta non può essere riprodotto per il tentativo di riautenticazione Codex",
|
||||
"codex_request_failed": "La richiesta Codex è fallita: %w",
|
||||
"codex_request_failed_status": "La richiesta Codex è fallita con stato %d",
|
||||
"codex_starting_browser_login": "Avvio dell'accesso OpenAI basato su browser per Codex.",
|
||||
"codex_token_exchange_failed": "Lo scambio di token Codex è fallito: %w",
|
||||
"codex_token_persist_failed": "impossibile salvare l'accesso Codex: %w",
|
||||
"codex_token_refresh_missing_access_token": "L'aggiornamento del token Codex non ha restituito un token di accesso.",
|
||||
"codex_usage_limit_reached": "Limite di utilizzo Codex raggiunto",
|
||||
"command_completed_successfully": "Comando completato con successo",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "Pattern personalizzati - Imposta la directory per i tuoi pattern personalizzati",
|
||||
"custom_patterns_warning_create_directory": "Avviso: Impossibile creare la directory dei modelli personalizzati %s: %v\n",
|
||||
"db_error_loading_env_file": "errore nel caricamento del file .env: %w",
|
||||
"db_error_updating_env_file": "errore nell'aggiornamento del file .env: %w",
|
||||
"defaults_model_context_length_question": "Inserisci la lunghezza del contesto del modello",
|
||||
"defaults_model_question": "Inserisci l'indice o il nome del tuo modello predefinito",
|
||||
"defaults_setup_description": "Fornitore e modello AI predefiniti",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "richiesta modelli DigitalOcean fallita con stato %d: %s",
|
||||
"disable_openai_responses_api": "Disabilita API OpenAI Responses (predefinito: false)",
|
||||
"disable_pattern_variable_replacement": "Disabilita sostituzione variabili pattern",
|
||||
"enable_web_search_tool": "Abilita strumento di ricerca web per modelli supportati (Anthropic, OpenAI, Gemini)",
|
||||
"enable_web_search_tool": "Abilita strumento di ricerca web per modelli supportati (Anthropic, OpenAI, Gemini, Grok)",
|
||||
"end_tag_thinking_sections": "Tag di fine per sezioni di pensiero",
|
||||
"error_creating_audio_file": "errore nella creazione del file audio: %v",
|
||||
"error_creating_file": "errore nella creazione del file: %v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "formato URL dati non valido",
|
||||
"ollama_invalid_http_timeout_using_default": "Timeout HTTP non valido '%s': %v, utilizzo del valore predefinito",
|
||||
"ollama_invalid_num_ctx_in_request": "num_ctx non valido nella richiesta: %v",
|
||||
"ollama_invalid_request_body": "corpo della richiesta non valido",
|
||||
"ollama_no_content_from_upstream": "nessun contenuto ricevuto dal server Fabric upstream",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx supera il valore massimo consentito di %d",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx deve essere un numero, ricevuto tipo non valido",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "La riga SSE supera il limite del buffer di 1MB - riga di dati troppo grande",
|
||||
"ollama_upstream_non_2xx": "Il server Fabric upstream ha restituito stato non-2xx %d: %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "Il server Fabric upstream ha restituito stato non-2xx %d e il corpo non è stato leggibile: %v",
|
||||
"ollama_upstream_request_failed": "impossibile raggiungere il server Fabric a monte",
|
||||
"ollama_upstream_returned_status": "il server Fabric upstream ha restituito stato %d",
|
||||
"ollama_warning_no_content": "Avviso: nessun contenuto ricevuto dal server Fabric upstream",
|
||||
"ollama_warning_parse_variables": "Avviso: impossibile analizzare options.variables come JSON: %v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "Preferisci playlist al video se entrambi gli ID sono presenti nell'URL",
|
||||
"print_context": "Stampa contesto",
|
||||
"print_current_version": "Stampa versione corrente",
|
||||
"print_metadata_to_stderr": "Stampa i metadati (token di input/output) su stderr",
|
||||
"print_pattern_contents": "Stampa il contenuto del pattern indicato nel terminale",
|
||||
"print_session": "Stampa sessione",
|
||||
"register_new_extension": "Registra una nuova estensione dal percorso del file di configurazione",
|
||||
"remove_registered_extension": "Rimuovi un'estensione registrata per nome",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "Invia notifica desktop quando il comando è completato",
|
||||
"serve_fabric_api_ollama_endpoints": "Servi l'API REST di Fabric con endpoint ollama",
|
||||
"serve_fabric_rest_api": "Servi l'API REST di Fabric",
|
||||
"server_api_key_required": "rifiuto di servire sull'indirizzo non loopback %s senza chiave API: impostare --api-key o FABRIC_API_KEY, oppure associare un indirizzo loopback come 127.0.0.1:8080",
|
||||
"server_chat_error": "Errore: %v",
|
||||
"server_error_marshaling_response": "errore nella serializzazione della risposta: %v",
|
||||
"server_error_writing_response": "errore nella scrittura della risposta: %v",
|
||||
"server_invalid_request_format": "formato della richiesta non valido: %v",
|
||||
"server_no_api_key_warning": "Avvio del server API REST senza autenticazione con chiave API. Ciò può comportare rischi per la sicurezza.",
|
||||
"sessions_creating_new": "Creazione nuova sessione: %s\n",
|
||||
"set_debug_level": "Imposta livello di debug (0=spento, 1=base, 2=dettagliato, 3=traccia, 4=wire)",
|
||||
"set_frequency_penalty": "Imposta penalità di frequenza",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**Show**: %s",
|
||||
"spotify_title_label": "**Titolo**: %s",
|
||||
"spotify_total_episodes_label": "**Episodi totali**: %d",
|
||||
"spotify_url_help": "URL di podcast o episodio Spotify da cui ottenere i metadati e inviarli alla chat",
|
||||
"spotify_url_label": "**URL**: %s",
|
||||
"start_tag_thinking_sections": "Tag di inizio per sezioni di pensiero",
|
||||
"storage_error_delete": "Impossibile eliminare %s: %v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "Impossibile salvare %s: %v",
|
||||
"storage_error_stat_entry": "Impossibile ottenere informazioni sulla voce %s: %v",
|
||||
"storage_error_unmarshal": "Impossibile deserializzare %s: %s",
|
||||
"storage_invalid_name": "nome non valido: %q",
|
||||
"strategies_available_header": "Strategie disponibili:",
|
||||
"strategies_cloning_repository": "Clonazione del repository %s (percorso: %s)...\\n",
|
||||
"strategies_download_success": "✅ Strategie scaricate e installate correttamente in %s\\n",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "利用可能なパターンからパターンを選択",
|
||||
"choose_session_from_available": "利用可能なセッションからセッションを選択",
|
||||
"choose_strategy_from_available": "利用可能な戦略から戦略を選択",
|
||||
"codex_api_base_url_question": "Codex APIベースURLを入力してください",
|
||||
"codex_auth_base_url_invalid": "Codex認証ベースURLが無効です: %w",
|
||||
"codex_auth_base_url_question": "Codex OAuthベースURLを入力してください",
|
||||
"codex_browser_open_fallback": "ブラウザが開かなかった場合は、このURLに移動して認証してください:",
|
||||
"codex_decode_models_response_failed": "Codexモデルレスポンスのデコードに失敗しました: %w",
|
||||
"codex_decode_refresh_response_failed": "更新されたCodexトークンレスポンスのデコードに失敗しました: %w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "安全なランダムOAuthステートの生成に失敗しました: %w",
|
||||
"codex_oauth_server_start_failed": "ローカルOAuthコールバックサーバーの起動に失敗しました: %w",
|
||||
"codex_oauth_state_mismatch": "ステートが一致しません",
|
||||
"codex_provider_error": "Codexベンダーエラー(ステータス %d): %s",
|
||||
"codex_refresh_failed_status": "Codexログインの更新に失敗しました(ステータス %d)",
|
||||
"codex_refresh_login_failed": "Codexログインの更新に失敗しました: %w",
|
||||
"codex_refresh_token_required": "Codexリフレッシュトークンが必要です。'fabric --setup'を再実行してください。",
|
||||
"codex_replay_body_unavailable": "Codex再認証リトライのためにリクエストボディを再送できません",
|
||||
"codex_request_failed": "Codexリクエストに失敗しました: %w",
|
||||
"codex_request_failed_status": "Codexリクエストがステータス %d で失敗しました",
|
||||
"codex_starting_browser_login": "Codex用のブラウザベースOpenAIログインを開始しています。",
|
||||
"codex_token_exchange_failed": "Codexトークン交換に失敗しました: %w",
|
||||
"codex_token_persist_failed": "Codexログインの保存に失敗しました: %w",
|
||||
"codex_token_refresh_missing_access_token": "Codexトークンの更新がアクセストークンを返しませんでした。",
|
||||
"codex_usage_limit_reached": "Codex使用量制限に達しました",
|
||||
"command_completed_successfully": "コマンドが正常に完了しました",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "カスタムパターン - カスタムパターン用のディレクトリを設定",
|
||||
"custom_patterns_warning_create_directory": "警告: カスタムパターンディレクトリ%sを作成できませんでした: %v\n",
|
||||
"db_error_loading_env_file": ".envファイルの読み込みエラー: %w",
|
||||
"db_error_updating_env_file": ".envファイルの更新エラー: %w",
|
||||
"defaults_model_context_length_question": "モデルのコンテキスト長を入力してください",
|
||||
"defaults_model_question": "デフォルトモデルのインデックスまたは名前を入力してください",
|
||||
"defaults_setup_description": "デフォルトのAIプロバイダーとモデル",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "DigitalOceanモデルリクエストがステータス%dで失敗しました: %s",
|
||||
"disable_openai_responses_api": "OpenAI Responses APIを無効化(デフォルト:false)",
|
||||
"disable_pattern_variable_replacement": "パターン変数の置換を無効化",
|
||||
"enable_web_search_tool": "サポートされているモデル(Anthropic、OpenAI、Gemini)でウェブ検索ツールを有効化",
|
||||
"enable_web_search_tool": "サポートされているモデル(Anthropic、OpenAI、Gemini、Grok)でウェブ検索ツールを有効化",
|
||||
"end_tag_thinking_sections": "思考セクションの終了タグ",
|
||||
"error_creating_audio_file": "音声ファイルの作成エラー: %v",
|
||||
"error_creating_file": "ファイルの作成エラー: %v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "無効なデータURLフォーマット",
|
||||
"ollama_invalid_http_timeout_using_default": "無効なHTTPタイムアウト '%s': %v、デフォルトを使用します",
|
||||
"ollama_invalid_num_ctx_in_request": "リクエストに無効な num_ctx があります: %v",
|
||||
"ollama_invalid_request_body": "無効なリクエスト本文",
|
||||
"ollama_no_content_from_upstream": "アップストリームの Fabric サーバーからコンテンツを受信しませんでした",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx が許可される最大値 %d を超えています",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx は数値である必要があります。無効な型を受け取りました",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "SSE 行が 1MB バッファー制限を超えています - データ行が大きすぎます",
|
||||
"ollama_upstream_non_2xx": "アップストリームの Fabric サーバーが非 2xx ステータス %d を返しました: %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "アップストリームの Fabric サーバーが非 2xx ステータス %d を返し、ボディを読み取れませんでした: %v",
|
||||
"ollama_upstream_request_failed": "上流のFabricサーバーに到達できませんでした",
|
||||
"ollama_upstream_returned_status": "アップストリームの Fabric サーバーがステータス %d を返しました",
|
||||
"ollama_warning_no_content": "警告: アップストリームの Fabric サーバーからコンテンツを受信しませんでした",
|
||||
"ollama_warning_parse_variables": "警告: options.variables を JSON として解析できませんでした: %v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "URLに両方のIDが存在する場合、動画よりプレイリストを優先",
|
||||
"print_context": "コンテキストを出力",
|
||||
"print_current_version": "現在のバージョンを出力",
|
||||
"print_metadata_to_stderr": "メタデータ(入力/出力トークン)を stderr に出力",
|
||||
"print_pattern_contents": "指定したパターンの内容をターミナルに出力",
|
||||
"print_session": "セッションを出力",
|
||||
"register_new_extension": "設定ファイルパスから新しい拡張機能を登録",
|
||||
"remove_registered_extension": "名前で登録済み拡張機能を削除",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "コマンド完了時にデスクトップ通知を送信",
|
||||
"serve_fabric_api_ollama_endpoints": "ollamaエンドポイント付きのFabric REST APIを提供",
|
||||
"serve_fabric_rest_api": "Fabric REST APIを提供",
|
||||
"server_api_key_required": "APIキーなしで非ループバックアドレス %s での提供を拒否しました。--api-key または FABRIC_API_KEY を設定するか、127.0.0.1:8080 のようなループバックアドレスにバインドしてください",
|
||||
"server_chat_error": "エラー: %v",
|
||||
"server_error_marshaling_response": "レスポンスのシリアライズエラー: %v",
|
||||
"server_error_writing_response": "レスポンスの書き込みエラー: %v",
|
||||
"server_invalid_request_format": "無効なリクエスト形式: %v",
|
||||
"server_no_api_key_warning": "APIキー認証なしでREST APIサーバーを起動しています。セキュリティ上のリスクが生じる可能性があります。",
|
||||
"sessions_creating_new": "新しいセッションを作成中: %s\n",
|
||||
"set_debug_level": "デバッグレベルを設定(0=オフ、1=基本、2=詳細、3=トレース、4=wire)",
|
||||
"set_frequency_penalty": "頻度ペナルティを設定",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**番組**: %s",
|
||||
"spotify_title_label": "**タイトル**: %s",
|
||||
"spotify_total_episodes_label": "**エピソード合計**: %d",
|
||||
"spotify_url_help": "メタデータを取得してチャットに送信する Spotify のポッドキャストまたはエピソードの URL",
|
||||
"spotify_url_label": "**URL**: %s",
|
||||
"start_tag_thinking_sections": "思考セクションの開始タグ",
|
||||
"storage_error_delete": "%sを削除できませんでした: %v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "%sを保存できませんでした: %v",
|
||||
"storage_error_stat_entry": "エントリ%sの情報を取得できませんでした: %v",
|
||||
"storage_error_unmarshal": "%sをデシリアライズできませんでした: %s",
|
||||
"storage_invalid_name": "無効な名前: %q",
|
||||
"strategies_available_header": "利用可能な戦略:",
|
||||
"strategies_cloning_repository": "リポジトリ %s をクローン中 (パス: %s)...\\n",
|
||||
"strategies_download_success": "✅ 戦略を %s に正常にダウンロードしてインストールしました\\n",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "Wybierz wzorzec spośród dostępnych wzorców",
|
||||
"choose_session_from_available": "Wybierz sesję spośród dostępnych sesji",
|
||||
"choose_strategy_from_available": "Wybierz strategię spośród dostępnych strategii",
|
||||
"codex_api_base_url_question": "Podaj bazowy URL API Codex",
|
||||
"codex_auth_base_url_invalid": "Nieprawidłowy bazowy URL uwierzytelniania Codex: %w",
|
||||
"codex_auth_base_url_question": "Podaj bazowy URL OAuth Codex",
|
||||
"codex_browser_open_fallback": "Jeśli przeglądarka się nie otworzyła, przejdź pod ten URL, aby się uwierzytelnić:",
|
||||
"codex_decode_models_response_failed": "Nie udało się zdekodować odpowiedzi modeli Codex: %w",
|
||||
"codex_decode_refresh_response_failed": "Nie udało się zdekodować odpowiedzi odświeżonego tokenu Codex: %w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "Nie udało się wygenerować bezpiecznego losowego stanu OAuth: %w",
|
||||
"codex_oauth_server_start_failed": "Nie udało się uruchomić lokalnego serwera zwrotnego OAuth: %w",
|
||||
"codex_oauth_state_mismatch": "Stan nie jest zgodny",
|
||||
"codex_provider_error": "błąd dostawcy Codex (status %d): %s",
|
||||
"codex_refresh_failed_status": "Nie udało się odświeżyć logowania Codex (status %d)",
|
||||
"codex_refresh_login_failed": "Nie udało się odświeżyć logowania Codex: %w",
|
||||
"codex_refresh_token_required": "Token odświeżania Codex jest wymagany. Uruchom ponownie 'fabric --setup'.",
|
||||
"codex_replay_body_unavailable": "Treść żądania nie może być odtworzona dla ponownej próby uwierzytelnienia Codex",
|
||||
"codex_request_failed": "Żądanie Codex nie powiodło się: %w",
|
||||
"codex_request_failed_status": "Żądanie Codex nie powiodło się ze statusem %d",
|
||||
"codex_starting_browser_login": "Uruchamianie logowania OpenAI przez przeglądarkę dla Codex.",
|
||||
"codex_token_exchange_failed": "Wymiana tokenu Codex nie powiodła się: %w",
|
||||
"codex_token_persist_failed": "nie udało się zapisać logowania Codex: %w",
|
||||
"codex_token_refresh_missing_access_token": "Odświeżenie tokenu Codex nie zwróciło tokenu dostępu.",
|
||||
"codex_usage_limit_reached": "Osiągnięto limit użycia Codex",
|
||||
"command_completed_successfully": "Polecenie zakończone pomyślnie",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "Niestandardowe wzorce - Ustaw katalog dla swoich niestandardowych wzorców",
|
||||
"custom_patterns_warning_create_directory": "Ostrzeżenie: Nie można utworzyć katalogu niestandardowych wzorców %s: %v\n",
|
||||
"db_error_loading_env_file": "błąd podczas ładowania pliku .env: %w",
|
||||
"db_error_updating_env_file": "błąd podczas aktualizacji pliku .env: %w",
|
||||
"defaults_model_context_length_question": "Podaj długość kontekstu modelu",
|
||||
"defaults_model_question": "Podaj indeks lub nazwę domyślnego modelu",
|
||||
"defaults_setup_description": "Domyślny dostawca AI i model",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "Żądanie modeli DigitalOcean nie powiodło się ze statusem %d: %s",
|
||||
"disable_openai_responses_api": "Wyłącz API odpowiedzi OpenAI (domyślnie: false)",
|
||||
"disable_pattern_variable_replacement": "Wyłącz zastępowanie zmiennych wzorców",
|
||||
"enable_web_search_tool": "Włącz narzędzie wyszukiwania internetowego dla obsługiwanych modeli (Anthropic, OpenAI, Gemini)",
|
||||
"enable_web_search_tool": "Włącz narzędzie wyszukiwania internetowego dla obsługiwanych modeli (Anthropic, OpenAI, Gemini, Grok)",
|
||||
"end_tag_thinking_sections": "Tag końcowy dla sekcji myślenia",
|
||||
"error_creating_audio_file": "błąd podczas tworzenia pliku audio: %v",
|
||||
"error_creating_file": "błąd podczas tworzenia pliku: %v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "nieprawidłowy format data URL",
|
||||
"ollama_invalid_http_timeout_using_default": "nieprawidłowy limit czasu HTTP '%s': %v, używam domyślnego",
|
||||
"ollama_invalid_num_ctx_in_request": "nieprawidłowa wartość num_ctx w żądaniu: %v",
|
||||
"ollama_invalid_request_body": "nieprawidłowa treść żądania",
|
||||
"ollama_no_content_from_upstream": "nie odebrano zawartości z upstream serwera fabric",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx przekracza maksymalną dozwoloną wartość %d",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx musi być liczbą, podano nieprawidłowy typ",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "Linia SSE przekracza limit bufora 1MB - linia danych zbyt duża",
|
||||
"ollama_upstream_non_2xx": "Upstream serwer fabric zwrócił status inny niż 2xx %d: %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "Upstream serwer fabric zwrócił status inny niż 2xx %d i nie można odczytać treści: %v",
|
||||
"ollama_upstream_request_failed": "nie można połączyć się z serwerem nadrzędnym Fabric",
|
||||
"ollama_upstream_returned_status": "upstream serwer fabric zwrócił status %d",
|
||||
"ollama_warning_no_content": "Ostrzeżenie: nie odebrano zawartości z upstream serwera fabric",
|
||||
"ollama_warning_parse_variables": "Ostrzeżenie: nie udało się przetworzyć options.variables jako JSON: %v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "Preferuj playlistę nad filmem, jeśli oba identyfikatory są obecne w URL",
|
||||
"print_context": "Wydrukuj kontekst",
|
||||
"print_current_version": "Wydrukuj bieżącą wersję",
|
||||
"print_metadata_to_stderr": "Wypisz metadane (tokeny wejściowe/wyjściowe) na stderr",
|
||||
"print_pattern_contents": "Wypisz zawartość wskazanego wzorca w terminalu",
|
||||
"print_session": "Wydrukuj sesję",
|
||||
"register_new_extension": "Zarejestruj nowe rozszerzenie z pliku konfiguracyjnego",
|
||||
"remove_registered_extension": "Usuń zarejestrowane rozszerzenie według nazwy",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "Wyślij powiadomienie pulpitu po zakończeniu polecenia",
|
||||
"serve_fabric_api_ollama_endpoints": "Uruchom fabric Rest API z endpointami ollama",
|
||||
"serve_fabric_rest_api": "Uruchom fabric Rest API",
|
||||
"server_api_key_required": "odmowa udostępniania na adresie %s spoza pętli zwrotnej bez klucza API: ustaw --api-key lub FABRIC_API_KEY, albo powiąż adres pętli zwrotnej, np. 127.0.0.1:8080",
|
||||
"server_chat_error": "Błąd: %v",
|
||||
"server_error_marshaling_response": "błąd podczas serializacji odpowiedzi: %v",
|
||||
"server_error_writing_response": "błąd podczas zapisywania odpowiedzi: %v",
|
||||
"server_invalid_request_format": "nieprawidłowy format żądania: %v",
|
||||
"server_no_api_key_warning": "Uruchamianie serwera REST API bez uwierzytelniania kluczem API. Może to stwarzać zagrożenia bezpieczeństwa.",
|
||||
"sessions_creating_new": "Tworzenie nowej sesji: %s\n",
|
||||
"set_debug_level": "Ustaw poziom debugowania (0=wyłączone, 1=podstawowe, 2=szczegółowe, 3=śledzenie, 4=surowe)",
|
||||
"set_frequency_penalty": "Ustaw karę częstotliwości",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**Program**: %s",
|
||||
"spotify_title_label": "**Tytuł**: %s",
|
||||
"spotify_total_episodes_label": "**Łączna liczba odcinków**: %d",
|
||||
"spotify_url_help": "URL podcastu lub odcinka Spotify do pobrania metadanych i wysłania do czatu",
|
||||
"spotify_url_label": "**URL**: %s",
|
||||
"start_tag_thinking_sections": "Tag początkowy dla sekcji myślenia",
|
||||
"storage_error_delete": "nie można usunąć %s: %v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "nie można zapisać %s: %v",
|
||||
"storage_error_stat_entry": "nie można pobrać informacji o wpisie %s: %v",
|
||||
"storage_error_unmarshal": "nie można deserializować %s: %s",
|
||||
"storage_invalid_name": "nieprawidłowa nazwa: %q",
|
||||
"strategies_available_header": "Dostępne strategie:",
|
||||
"strategies_cloning_repository": "Klonowanie repozytorium %s (ścieżka: %s)...\n",
|
||||
"strategies_download_success": "✅ Pomyślnie pobrano i zainstalowano strategie w %s\n",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "Escolha um padrão entre os padrões disponíveis",
|
||||
"choose_session_from_available": "Escolha uma sessão das sessões disponíveis",
|
||||
"choose_strategy_from_available": "Escolher uma estratégia das estratégias disponíveis",
|
||||
"codex_api_base_url_question": "Insira a URL base da API do Codex",
|
||||
"codex_auth_base_url_invalid": "URL base de autenticação do Codex inválida: %w",
|
||||
"codex_auth_base_url_question": "Insira a URL base de OAuth do Codex",
|
||||
"codex_browser_open_fallback": "Se o navegador não abriu, navegue até esta URL para se autenticar:",
|
||||
"codex_decode_models_response_failed": "Falha ao decodificar a resposta de modelos do Codex: %w",
|
||||
"codex_decode_refresh_response_failed": "Falha ao decodificar a resposta do token atualizado do Codex: %w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "Falha ao gerar estado OAuth aleatório seguro: %w",
|
||||
"codex_oauth_server_start_failed": "Falha ao iniciar o servidor local de callback OAuth: %w",
|
||||
"codex_oauth_state_mismatch": "O estado não corresponde",
|
||||
"codex_provider_error": "erro do fornecedor Codex (status %d): %s",
|
||||
"codex_refresh_failed_status": "Falha ao atualizar o login do Codex (status %d)",
|
||||
"codex_refresh_login_failed": "Falha ao atualizar o login do Codex: %w",
|
||||
"codex_refresh_token_required": "O token de atualização do Codex é obrigatório. Execute 'fabric --setup' novamente.",
|
||||
"codex_replay_body_unavailable": "O corpo da requisição não pode ser reproduzido para a tentativa de reautenticação do Codex",
|
||||
"codex_request_failed": "A requisição do Codex falhou: %w",
|
||||
"codex_request_failed_status": "A requisição do Codex falhou com status %d",
|
||||
"codex_starting_browser_login": "Iniciando login OpenAI baseado em navegador para o Codex.",
|
||||
"codex_token_exchange_failed": "A troca de token do Codex falhou: %w",
|
||||
"codex_token_persist_failed": "falha ao persistir o login do Codex: %w",
|
||||
"codex_token_refresh_missing_access_token": "A atualização do token do Codex não retornou um token de acesso.",
|
||||
"codex_usage_limit_reached": "Limite de uso do Codex atingido",
|
||||
"command_completed_successfully": "Comando concluído com sucesso",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "Padrões personalizados - Definir diretório para seus padrões personalizados",
|
||||
"custom_patterns_warning_create_directory": "Aviso: Não foi possível criar o diretório de padrões personalizados %s: %v\n",
|
||||
"db_error_loading_env_file": "erro ao carregar o arquivo .env: %w",
|
||||
"db_error_updating_env_file": "erro ao atualizar o arquivo .env: %w",
|
||||
"defaults_model_context_length_question": "Informe o comprimento do contexto do modelo",
|
||||
"defaults_model_question": "Informe o índice ou o nome do seu modelo padrão",
|
||||
"defaults_setup_description": "Provedor e modelo de IA padrão",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "requisição de modelos do DigitalOcean falhou com status %d: %s",
|
||||
"disable_openai_responses_api": "Desabilitar API OpenAI Responses (padrão: false)",
|
||||
"disable_pattern_variable_replacement": "Desabilitar substituição de variáveis de padrão",
|
||||
"enable_web_search_tool": "Habilitar ferramenta de busca web para modelos suportados (Anthropic, OpenAI, Gemini)",
|
||||
"enable_web_search_tool": "Habilitar ferramenta de busca web para modelos suportados (Anthropic, OpenAI, Gemini, Grok)",
|
||||
"end_tag_thinking_sections": "Tag final para seções de pensamento",
|
||||
"error_creating_audio_file": "erro ao criar arquivo de áudio: %v",
|
||||
"error_creating_file": "erro ao criar arquivo: %v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "formato de URL de dados inválido",
|
||||
"ollama_invalid_http_timeout_using_default": "Tempo limite HTTP inválido '%s': %v, usando o padrão",
|
||||
"ollama_invalid_num_ctx_in_request": "num_ctx inválido na requisição: %v",
|
||||
"ollama_invalid_request_body": "corpo de solicitação inválido",
|
||||
"ollama_no_content_from_upstream": "nenhum conteúdo recebido do servidor Fabric upstream",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx excede o valor máximo permitido de %d",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx deve ser um número, recebeu tipo inválido",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "Linha SSE excede limite de buffer de 1MB - linha de dados muito grande",
|
||||
"ollama_upstream_non_2xx": "Servidor Fabric upstream retornou status não-2xx %d: %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "Servidor Fabric upstream retornou status não-2xx %d e o corpo não pôde ser lido: %v",
|
||||
"ollama_upstream_request_failed": "falha ao conectar ao servidor Fabric upstream",
|
||||
"ollama_upstream_returned_status": "servidor Fabric upstream retornou status %d",
|
||||
"ollama_warning_no_content": "Aviso: nenhum conteúdo recebido do servidor Fabric upstream",
|
||||
"ollama_warning_parse_variables": "Aviso: falha ao analisar options.variables como JSON: %v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "Preferir playlist ao vídeo se ambos os IDs estiverem presentes na URL",
|
||||
"print_context": "Imprimir contexto",
|
||||
"print_current_version": "Imprimir versão atual",
|
||||
"print_metadata_to_stderr": "Imprimir metadados (tokens de entrada/saída) no stderr",
|
||||
"print_pattern_contents": "Imprimir o conteúdo do padrão indicado no terminal",
|
||||
"print_session": "Imprimir sessão",
|
||||
"register_new_extension": "Registrar uma nova extensão do caminho do arquivo de configuração",
|
||||
"remove_registered_extension": "Remover uma extensão registrada por nome",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "Enviar notificação desktop quando o comando for concluído",
|
||||
"serve_fabric_api_ollama_endpoints": "Servir a API REST do Fabric com endpoints ollama",
|
||||
"serve_fabric_rest_api": "Servir a API REST do Fabric",
|
||||
"server_api_key_required": "recusando servir no endereço não loopback %s sem chave de API: defina --api-key ou FABRIC_API_KEY, ou vincule um endereço loopback como 127.0.0.1:8080",
|
||||
"server_chat_error": "Erro: %v",
|
||||
"server_error_marshaling_response": "erro ao serializar resposta: %v",
|
||||
"server_error_writing_response": "erro ao escrever resposta: %v",
|
||||
"server_invalid_request_format": "formato de solicitação inválido: %v",
|
||||
"server_no_api_key_warning": "Iniciando o servidor da API REST sem autenticação por chave de API. Isso pode representar riscos de segurança.",
|
||||
"sessions_creating_new": "Criando nova sessão: %s\n",
|
||||
"set_debug_level": "Definir nível de debug (0=desligado, 1=básico, 2=detalhado, 3=rastreamento, 4=wire)",
|
||||
"set_frequency_penalty": "Definir penalidade de frequência",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**Programa**: %s",
|
||||
"spotify_title_label": "**Título**: %s",
|
||||
"spotify_total_episodes_label": "**Total de episódios**: %d",
|
||||
"spotify_url_help": "URL de podcast ou episódio do Spotify para obter metadados e enviar ao chat",
|
||||
"spotify_url_label": "**URL**: %s",
|
||||
"start_tag_thinking_sections": "Tag inicial para seções de pensamento",
|
||||
"storage_error_delete": "Não foi possível excluir %s: %v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "Não foi possível salvar %s: %v",
|
||||
"storage_error_stat_entry": "Não foi possível obter informações da entrada %s: %v",
|
||||
"storage_error_unmarshal": "Não foi possível desserializar %s: %s",
|
||||
"storage_invalid_name": "nome inválido: %q",
|
||||
"strategies_available_header": "Estratégias disponíveis:",
|
||||
"strategies_cloning_repository": "Clonando repositório %s (caminho: %s)...\\n",
|
||||
"strategies_download_success": "✅ Estratégias baixadas e instaladas com sucesso em %s\\n",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "Escolha um padrão dos padrões disponíveis",
|
||||
"choose_session_from_available": "Escolha uma sessão das sessões disponíveis",
|
||||
"choose_strategy_from_available": "Escolher uma estratégia das estratégias disponíveis",
|
||||
"codex_api_base_url_question": "Introduza o URL base da API do Codex",
|
||||
"codex_auth_base_url_invalid": "URL base de autenticação do Codex inválido: %w",
|
||||
"codex_auth_base_url_question": "Introduza o URL base de OAuth do Codex",
|
||||
"codex_browser_open_fallback": "Se o navegador não abriu, navegue até este URL para se autenticar:",
|
||||
"codex_decode_models_response_failed": "Falha ao descodificar a resposta de modelos do Codex: %w",
|
||||
"codex_decode_refresh_response_failed": "Falha ao descodificar a resposta do token atualizado do Codex: %w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "Falha ao gerar estado OAuth aleatório seguro: %w",
|
||||
"codex_oauth_server_start_failed": "Falha ao iniciar o servidor local de retorno OAuth: %w",
|
||||
"codex_oauth_state_mismatch": "O estado não corresponde",
|
||||
"codex_provider_error": "erro do fornecedor Codex (estado %d): %s",
|
||||
"codex_refresh_failed_status": "Falha ao atualizar o início de sessão do Codex (estado %d)",
|
||||
"codex_refresh_login_failed": "Falha ao atualizar o início de sessão do Codex: %w",
|
||||
"codex_refresh_token_required": "O token de atualização do Codex é obrigatório. Execute 'fabric --setup' novamente.",
|
||||
"codex_replay_body_unavailable": "O corpo do pedido não pode ser reproduzido para a tentativa de reautenticação do Codex",
|
||||
"codex_request_failed": "O pedido do Codex falhou: %w",
|
||||
"codex_request_failed_status": "O pedido do Codex falhou com estado %d",
|
||||
"codex_starting_browser_login": "A iniciar início de sessão OpenAI baseado no navegador para o Codex.",
|
||||
"codex_token_exchange_failed": "A troca de token do Codex falhou: %w",
|
||||
"codex_token_persist_failed": "falha ao persistir o início de sessão do Codex: %w",
|
||||
"codex_token_refresh_missing_access_token": "A atualização do token do Codex não devolveu um token de acesso.",
|
||||
"codex_usage_limit_reached": "Limite de utilização do Codex atingido",
|
||||
"command_completed_successfully": "Comando concluído com sucesso",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "Padrões personalizados - Definir diretório para os seus padrões personalizados",
|
||||
"custom_patterns_warning_create_directory": "Aviso: Não foi possível criar o diretório de padrões personalizados %s: %v\n",
|
||||
"db_error_loading_env_file": "erro ao carregar o ficheiro .env: %w",
|
||||
"db_error_updating_env_file": "erro ao atualizar o ficheiro .env: %w",
|
||||
"defaults_model_context_length_question": "Indique o comprimento do contexto do modelo",
|
||||
"defaults_model_question": "Indique o índice ou o nome do seu modelo padrão",
|
||||
"defaults_setup_description": "Fornecedor e modelo de IA padrão",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "pedido de modelos do DigitalOcean falhou com estado %d: %s",
|
||||
"disable_openai_responses_api": "Desabilitar API OpenAI Responses (por omissão: false)",
|
||||
"disable_pattern_variable_replacement": "Desabilitar substituição de variáveis de padrão",
|
||||
"enable_web_search_tool": "Habilitar ferramenta de pesquisa web para modelos suportados (Anthropic, OpenAI, Gemini)",
|
||||
"enable_web_search_tool": "Habilitar ferramenta de pesquisa web para modelos suportados (Anthropic, OpenAI, Gemini, Grok)",
|
||||
"end_tag_thinking_sections": "Tag final para secções de pensamento",
|
||||
"error_creating_audio_file": "erro ao criar ficheiro de áudio: %v",
|
||||
"error_creating_file": "erro ao criar ficheiro: %v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "formato de URL de dados inválido",
|
||||
"ollama_invalid_http_timeout_using_default": "Tempo limite HTTP inválido '%s': %v, a utilizar o padrão",
|
||||
"ollama_invalid_num_ctx_in_request": "num_ctx inválido no pedido: %v",
|
||||
"ollama_invalid_request_body": "corpo de pedido inválido",
|
||||
"ollama_no_content_from_upstream": "nenhum conteúdo recebido do servidor Fabric upstream",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx excede o valor máximo permitido de %d",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx deve ser um número, recebeu tipo inválido",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "Linha SSE excede limite de buffer de 1MB - linha de dados demasiado grande",
|
||||
"ollama_upstream_non_2xx": "Servidor Fabric upstream retornou estado não-2xx %d: %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "Servidor Fabric upstream retornou estado não-2xx %d e o corpo não pôde ser lido: %v",
|
||||
"ollama_upstream_request_failed": "falha ao contactar o servidor Fabric a montante",
|
||||
"ollama_upstream_returned_status": "servidor Fabric upstream retornou estado %d",
|
||||
"ollama_warning_no_content": "Aviso: nenhum conteúdo recebido do servidor Fabric upstream",
|
||||
"ollama_warning_parse_variables": "Aviso: falha ao analisar options.variables como JSON: %v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "Preferir playlist ao vídeo se ambos os IDs estiverem presentes na URL",
|
||||
"print_context": "Imprimir contexto",
|
||||
"print_current_version": "Imprimir versão atual",
|
||||
"print_metadata_to_stderr": "Imprimir metadados (tokens de entrada/saída) no stderr",
|
||||
"print_pattern_contents": "Imprimir o conteúdo do padrão indicado no terminal",
|
||||
"print_session": "Imprimir sessão",
|
||||
"register_new_extension": "Registar uma nova extensão do caminho do ficheiro de configuração",
|
||||
"remove_registered_extension": "Remover uma extensão registada por nome",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "Enviar notificação no ambiente de trabalho quando o comando for concluído",
|
||||
"serve_fabric_api_ollama_endpoints": "Servir a API REST do Fabric com endpoints ollama",
|
||||
"serve_fabric_rest_api": "Servir a API REST do Fabric",
|
||||
"server_api_key_required": "recusa de servir no endereço não loopback %s sem chave de API: defina --api-key ou FABRIC_API_KEY, ou vincule um endereço loopback como 127.0.0.1:8080",
|
||||
"server_chat_error": "Erro: %v",
|
||||
"server_error_marshaling_response": "erro ao serializar resposta: %v",
|
||||
"server_error_writing_response": "erro ao escrever resposta: %v",
|
||||
"server_invalid_request_format": "formato de pedido inválido: %v",
|
||||
"server_no_api_key_warning": "A iniciar o servidor da API REST sem autenticação por chave de API. Isto pode representar riscos de segurança.",
|
||||
"sessions_creating_new": "A criar nova sessão: %s\n",
|
||||
"set_debug_level": "Definir nível de debug (0=desligado, 1=básico, 2=detalhado, 3=rastreio, 4=wire)",
|
||||
"set_frequency_penalty": "Definir penalidade de frequência",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**Programa**: %s",
|
||||
"spotify_title_label": "**Título**: %s",
|
||||
"spotify_total_episodes_label": "**Total de episódios**: %d",
|
||||
"spotify_url_help": "URL de podcast ou episódio do Spotify para obter metadados e enviar ao chat",
|
||||
"spotify_url_label": "**URL**: %s",
|
||||
"start_tag_thinking_sections": "Tag inicial para secções de pensamento",
|
||||
"storage_error_delete": "Não foi possível eliminar %s: %v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "Não foi possível guardar %s: %v",
|
||||
"storage_error_stat_entry": "Não foi possível obter informações da entrada %s: %v",
|
||||
"storage_error_unmarshal": "Não foi possível desserializar %s: %s",
|
||||
"storage_invalid_name": "nome inválido: %q",
|
||||
"strategies_available_header": "Estratégias disponíveis:",
|
||||
"strategies_cloning_repository": "A clonar repositório %s (caminho: %s)...\\n",
|
||||
"strategies_download_success": "✅ Estratégias transferidas e instaladas com sucesso em %s\\n",
|
||||
|
|
|
|||
770
internal/i18n/locales/tr.json
Normal file
770
internal/i18n/locales/tr.json
Normal file
|
|
@ -0,0 +1,770 @@
|
|||
{
|
||||
"abacus_models_endpoint_status": "abacus modelleri uç noktası %d durumunu döndürdü",
|
||||
"additional_yt_dlp_args": "yt-dlp'ye geçirilecek ek argümanlar (örn. '--cookies-from-browser brave')",
|
||||
"address_to_bind_rest_api": "REST API'sini bağlamak için adres",
|
||||
"anthropic_stream_error": "Akış hatası: %v",
|
||||
"api_key_secure_server_routes": "Sunucu rotalarını güvence altına almak için kullanılan API anahtarı",
|
||||
"application_options_header": "Uygulama Seçenekleri:",
|
||||
"apply_variables_to_input": "Değişkenleri kullanıcı girişine uygula",
|
||||
"attachment_could_not_determine_mimetype": "URL'nin mimetype'ı belirlenemedi",
|
||||
"attachment_file_not_exist": "dosya %s mevcut değil",
|
||||
"attachment_no_content_available": "içerik mevcut değil",
|
||||
"attachment_no_type_no_content": "eklentiye ait tür veya türetilecek içerik mevcut değil",
|
||||
"attachment_path_or_url_help": "Eklenti yolu veya URL'si (örn. OpenAI görsel tanıma mesajları için)",
|
||||
"audio_output_file_specified_but_not_tts_model": "ses çıkış dosyası '%s' belirtildi ancak '%s' modeli bir TTS modeli değil. Lütfen gemini-2.5-flash-preview-tts gibi bir TTS modeli kullanın",
|
||||
"audio_video_file_transcribe": "Ses veya video dosyasını yazıya dönüştür",
|
||||
"available_models_header": "Mevcut modeller",
|
||||
"available_transcription_models": "Mevcut yazıya dönüştürme modelleri:",
|
||||
"available_vendors_header": "Mevcut Sağlayıcılar:",
|
||||
"azure_api_key_required": "Azure API anahtarı gerekli",
|
||||
"azure_api_version_question": "Azure API sürümünü girin (varsayılan için boş bırakın)",
|
||||
"azure_base_url_question": "API Temel URL'si",
|
||||
"azure_base_url_required": "Azure temel URL'si gerekli",
|
||||
"azure_credential_failure": "Azure kimlik bilgisi oluşturulamadı",
|
||||
"azure_deployments_question": "Azure dağıtım adlarınızı girin (virgülle ayrılmış)",
|
||||
"azure_deployments_required": "en az bir Azure dağıtım adı gerekli",
|
||||
"azure_failed_extract_deployment": "istekten dağıtım adı alınamadı",
|
||||
"azure_model_field_empty": "istek gövdesinde model alanı boş",
|
||||
"azure_request_body_nil": "istek gövdesi boş",
|
||||
"azureaigateway_aoai_no_choices": "Azure OpenAI yanıtında seçenek yok",
|
||||
"azureaigateway_aoai_parse_response_failed": "Azure OpenAI yanıtı ayrıştırılamadı: %w",
|
||||
"azureaigateway_api_version_question": "Azure OpenAI API sürümü (varsayılan: 2025-04-01-preview, varsayılan için boş bırakın)",
|
||||
"azureaigateway_backend_not_initialized": "arka uç başlatılmadı - yapılandırmak için 'fabric --setup' komutunu çalıştırın",
|
||||
"azureaigateway_backend_type_question": "Arka uç türünü seçin (bedrock, azure-openai, vertex-ai)",
|
||||
"azureaigateway_bedrock_no_text_blocks": "Bedrock yanıtında metin içerik bloğu yok",
|
||||
"azureaigateway_bedrock_parse_response_failed": "Bedrock yanıtı ayrıştırılamadı: %w",
|
||||
"azureaigateway_failed_create_request": "AzureAIGateway: istek oluşturulamadı: %w",
|
||||
"azureaigateway_failed_read_response": "AzureAIGateway: yanıt okunamadı: %w",
|
||||
"azureaigateway_gateway_url_https_required": "ağ geçidi URL'si HTTPS şeması kullanmalıdır",
|
||||
"azureaigateway_gateway_url_question": "Azure APIM Ağ Geçidi temel URL'nizi girin (örn. https://gateway.company.com)",
|
||||
"azureaigateway_gateway_url_required": "Azure APIM ağ geçidi URL'si gerekli",
|
||||
"azureaigateway_http_error": "AzureAIGateway: HTTP %d: %s",
|
||||
"azureaigateway_http_request_failed": "AzureAIGateway: HTTP isteği başarısız oldu: %w",
|
||||
"azureaigateway_invalid_gateway_url": "geçersiz ağ geçidi URL'si: %w",
|
||||
"azureaigateway_no_valid_messages": "boş içerik filtrelendikten sonra geçerli mesaj yok",
|
||||
"azureaigateway_prepare_request_failed": "AzureAIGateway: %w",
|
||||
"azureaigateway_response_too_large": "AzureAIGateway: yanıt çok büyük (>%d bayt)",
|
||||
"azureaigateway_subscription_key_question": "Azure APIM abonelik anahtarınızı girin",
|
||||
"azureaigateway_subscription_key_required": "Azure APIM abonelik anahtarı gerekli",
|
||||
"azureaigateway_unsupported_backend": "desteklenmeyen arka uç: %s (geçerli seçenekler: bedrock, azure-openai, vertex-ai)",
|
||||
"azureaigateway_vertexai_no_content": "Vertex AI yanıtında içerik yok",
|
||||
"azureaigateway_vertexai_parse_response_failed": "Vertex AI yanıtı ayrıştırılamadı: %w",
|
||||
"background_type_help": "Arka plan türü: opak, şeffaf (varsayılan: opak, yalnızca PNG/WebP için)",
|
||||
"bedrock_api_key_label": "Bedrock API Anahtarınızı / ABSK belirtecinizi girin (önerilir — Claude Code tarafından kullanılan anahtarla aynı)",
|
||||
"bedrock_aws_access_key_label": "AWS Erişim Anahtarı Kimliğinizi girin (yalnızca yukarıdaki API Anahtarını kullanmıyorsanız)",
|
||||
"bedrock_aws_region_label": "AWS Bölgenizi girin (örn. us-east-1, us-west-2, eu-west-1, ap-southeast-1)",
|
||||
"bedrock_aws_secret_key_label": "AWS Gizli Erişim Anahtarınızı girin (yalnızca yukarıdaki API Anahtarını kullanmıyorsanız)",
|
||||
"bedrock_client_not_initialized": "Bedrock istemcisi başlatılmadı — yapılandırmak için 'fabric --setup' komutunu çalıştırın",
|
||||
"bedrock_converse_failed": "bedrock converse, %s modeli için başarısız oldu: %w",
|
||||
"bedrock_conversestream_failed": "bedrock conversestream, %s modeli için başarısız oldu: %w",
|
||||
"bedrock_empty_response_content": "boş yanıt içeriği",
|
||||
"bedrock_failed_list_foundation_models": "temel modeller listelenemedi: %w",
|
||||
"bedrock_failed_list_inference_profiles": "çıkarım profilleri listelenemedi: %w",
|
||||
"bedrock_fetch_regions_bad_status": "botocore uç noktaları beklenmeyen bir durum döndürdü",
|
||||
"bedrock_fetch_regions_failed": "botocore'dan Bedrock bölgeleri alınamadı",
|
||||
"bedrock_fetch_regions_parse_failed": "botocore endpoints.json ayrıştırılamadı",
|
||||
"bedrock_invalid_aws_region": "geçersiz AWS bölgesi: %s",
|
||||
"bedrock_listmodels_fallback": "Bedrock ListModels API başarısız oldu, statik yedek kullanılıyor",
|
||||
"bedrock_panic_sendstream": "SendStream'de panik: %v",
|
||||
"bedrock_setup_auth_option_accesskey": " [2] AWS Erişim Anahtarı + Gizli Anahtar",
|
||||
"bedrock_setup_auth_option_apikey": " [1] Bedrock API Anahtarı / ABSK belirteci (önerilir — Claude Code ile aynı)",
|
||||
"bedrock_setup_auth_prompt": "1 veya 2 girin",
|
||||
"bedrock_setup_choose_auth_method": " Kimlik doğrulama yöntemini seçin:",
|
||||
"bedrock_setup_choose_model": " Varsayılan modeli seçin (ön ek olmayan kimlikler herhangi bir bölgede çalışır; us./eu./ap. bölgeye özeldir):",
|
||||
"bedrock_setup_choose_region": " AWS Bölgesini seçin:",
|
||||
"bedrock_setup_header": "[Bedrock]",
|
||||
"bedrock_setup_invalid_auth_selection": "geçersiz seçim: %s (1 veya 2 girin)",
|
||||
"bedrock_setup_model_custom_prompt": "Model kimliğini girin (örn. anthropic.claude-opus-4-6-v1)",
|
||||
"bedrock_setup_model_option_custom": " [0] Farklı bir model kimliği girin",
|
||||
"bedrock_setup_model_prompt": "Model numarasını veya kendinizinkini yazmak için 0 girin",
|
||||
"bedrock_setup_region_custom_prompt": "Özel AWS bölgesini girin (örn. us-east-1)",
|
||||
"bedrock_setup_region_option_custom": " [0] Farklı bir bölge girin",
|
||||
"bedrock_setup_region_prompt": "Bölge numarasını veya özel için 0 girin",
|
||||
"bedrock_setup_selected_model": " ✓ Seçilen model: %s",
|
||||
"bedrock_setup_use_with": " ℹ Şununla kullanın: fabric -m %s -V Bedrock",
|
||||
"bedrock_unable_load_aws_config": "AWS Yapılandırması yüklenemiyor: %w",
|
||||
"bedrock_unable_load_aws_config_with_region": "AWS Yapılandırması %s bölgesiyle yüklenemiyor: %w",
|
||||
"bedrock_unexpected_content_block_type": "beklenmeyen içerik bloğu türü: %T",
|
||||
"bedrock_unexpected_response_type": "beklenmeyen yanıt türü: %T",
|
||||
"bedrock_unknown_stream_event_type": "bilinmeyen akış olayı türü: %T",
|
||||
"cannot_convert_string": "%q dizgisi %v öğesine dönüştürülemiyor",
|
||||
"change_default_model": "Varsayılan modeli değiştir",
|
||||
"chat_error_content_fields_misused": "Content ve MultiContent özelliklerini aynı anda kullanamazsınız",
|
||||
"chatter_error_empty_response": "boş yanıt",
|
||||
"chatter_error_find_context": "%s bağlamı bulunamadı: %v",
|
||||
"chatter_error_find_session": "%s oturumu bulunamadı: %v",
|
||||
"chatter_error_get_pattern": "%s deseni alınamadı: %v",
|
||||
"chatter_error_load_strategy": "%s stratejisi yüklenemedi: %v",
|
||||
"chatter_error_no_messages_provided": "mesaj sağlanmadı",
|
||||
"chatter_error_no_session_pattern_user_messages": "oturum, desen veya kullanıcı mesajı sağlanmadı",
|
||||
"chatter_error_stream_update": "Hata: %s",
|
||||
"chatter_help_review_changes_with_git_diff": "git kullanıyorsanız değişiklikleri 'git diff' ile inceleyebilirsiniz.",
|
||||
"chatter_info_file_changes_applied_successfully": "Dosya değişiklikleri başarıyla uygulandı.",
|
||||
"chatter_log_stream_usage_metadata": "[Meta Veri] Giriş: %d | Çıkış: %d | Toplam: %d",
|
||||
"chatter_prompt_enforce_response_language": "%s\n\nÖNEMLİ: Öncelikle, bu istemde verilen talimatları kullanıcının girdisini kullanarak yürütün. İkinci olarak, talimatların yürütülmesinin bir parçası olarak oluşturulan herhangi bir bölüm başlığı veya başlık dahil olmak üzere tüm nihai yanıtınızın YALNIZCA %s dilinde yazıldığından emin olun.",
|
||||
"chatter_warning_apply_file_changes_failed": "Uyarı: Dosya değişiklikleri uygulanamadı: %v",
|
||||
"chatter_warning_get_current_directory_failed": "Uyarı: Geçerli dizin alınamadı: %v",
|
||||
"chatter_warning_parse_file_changes_failed": "Uyarı: Dosya değişiklikleri ayrıştırılamadı: %v",
|
||||
"choose_context_from_available": "Mevcut bağlamlar arasından bir bağlam seçin",
|
||||
"choose_model": "Model seç",
|
||||
"choose_pattern_from_available": "Mevcut desenler arasından bir desen seçin",
|
||||
"choose_session_from_available": "Mevcut oturumlar arasından bir oturum seçin",
|
||||
"choose_strategy_from_available": "Mevcut stratejiler arasından bir strateji seçin",
|
||||
"codex_api_base_url_question": "Codex API temel URL'nizi girin",
|
||||
"codex_auth_base_url_invalid": "geçersiz codex kimlik doğrulama temel URL'si: %w",
|
||||
"codex_auth_base_url_question": "Codex OAuth temel URL'nizi girin",
|
||||
"codex_browser_open_fallback": "Tarayıcınız açılmadıysa, kimlik doğrulaması için bu URL'ye gidin:",
|
||||
"codex_decode_models_response_failed": "codex modelleri yanıtı çözülemedi: %w",
|
||||
"codex_decode_refresh_response_failed": "yenilenmiş Codex belirteç yanıtı çözülemedi: %w",
|
||||
"codex_decode_token_response_failed": "codex belirteç takas yanıtı çözülemedi: %w",
|
||||
"codex_image_file_not_supported": "Codex sağlayıcısı --image-file özelliğini desteklemiyor. Bunun yerine bir görsel eki kullanın.",
|
||||
"codex_login_account_changed": "Codex girişi, depolanan yapılandırmadan farklı bir ChatGPT hesabına bağlı. Lütfen 'fabric --setup' komutunu tekrar çalıştırın.",
|
||||
"codex_login_completed": "Codex girişi tamamlandı",
|
||||
"codex_login_failed": "Codex girişi başarısız oldu: %s",
|
||||
"codex_login_invalid": "Codex girişi artık geçerli değil. Lütfen 'fabric --setup' komutunu tekrar çalıştırın.",
|
||||
"codex_login_missing_account_claim": "Codex girişi bir ChatGPT hesap kimliği içermiyordu. Bu giriş durumu desteklenmiyor.",
|
||||
"codex_login_missing_auth_code": "Codex girişi bir yetkilendirme kodu döndürmedi.",
|
||||
"codex_login_missing_tokens": "Codex girişi gerekli erişim ve yenileme belirteçlerini döndürmedi.",
|
||||
"codex_login_refresh_failed": "Codex girişi yenilenemedi. Lütfen 'fabric --setup' komutunu tekrar çalıştırın.",
|
||||
"codex_login_return_to_fabric": "Fabric'e dönün.",
|
||||
"codex_login_revoked": "Codex girişi süresi doldu veya iptal edildi. Lütfen 'fabric --setup' komutunu tekrar çalıştırın.",
|
||||
"codex_login_server_stopped": "Codex giriş geri çağırma sunucusu, kimlik doğrulama tamamlanmadan önce durdu.",
|
||||
"codex_login_state_mismatch": "OAuth durumu eşleşmediği için Codex girişi doğrulanamadı.",
|
||||
"codex_login_timed_out": "Codex girişi, kimlik doğrulama tamamlanmadan önce zaman aşımına uğradı.",
|
||||
"codex_oauth_missing_auth_code": "Yetkilendirme kodu eksik",
|
||||
"codex_oauth_random_state_failed": "güvenli rastgele oauth durumu oluşturulamadı: %w",
|
||||
"codex_oauth_server_start_failed": "yerel oauth geri çağırma sunucusu başlatılamadı: %w",
|
||||
"codex_oauth_state_mismatch": "Durum eşleşmiyor",
|
||||
"codex_provider_error": "codex sağlayıcı hatası (durum %d): %s",
|
||||
"codex_refresh_failed_status": "codex girişi yenilenemedi (durum %d)",
|
||||
"codex_refresh_login_failed": "Codex girişi yenilenemedi: %w",
|
||||
"codex_refresh_token_required": "Codex yenileme belirteci gerekli. Lütfen 'fabric --setup' komutunu tekrar çalıştırın.",
|
||||
"codex_replay_body_unavailable": "Codex yeniden kimlik doğrulama denemesi için istek gövdesi tekrar oynatılamaz",
|
||||
"codex_request_failed": "codex isteği başarısız oldu: %w",
|
||||
"codex_request_failed_status": "codex isteği %d durumuyla başarısız oldu",
|
||||
"codex_starting_browser_login": "Codex için tarayıcı tabanlı OpenAI girişi başlatılıyor.",
|
||||
"codex_token_exchange_failed": "codex belirteç takası başarısız oldu: %w",
|
||||
"codex_token_persist_failed": "Codex girişi kalıcı hale getirilemedi: %w",
|
||||
"codex_token_refresh_missing_access_token": "Codex belirteç yenilemesi bir erişim belirteci döndürmedi.",
|
||||
"codex_usage_limit_reached": "codex kullanım limitine ulaşıldı",
|
||||
"command_completed_successfully": "Komut başarıyla tamamlandı",
|
||||
"compression_level_jpeg_webp": "JPEG/WebP biçimleri için sıkıştırma seviyesi 0-100 (varsayılan: ayarlanmadı)",
|
||||
"config_file_not_found": "yapılandırma dosyası bulunamadı: %s",
|
||||
"convert_html_readability": "HTML girdisini temiz, okunabilir bir görünüme dönüştür",
|
||||
"copilot_debug_created_conversation": "Copilot konuşması oluşturuldu: %s",
|
||||
"copilot_debug_failed_parse_sse_event": "SSE olayı ayrıştırılamadı: %v",
|
||||
"copilot_error_chat_request": "sohbet isteği başarısız oldu: %s - %s",
|
||||
"copilot_error_create_conversation": "konuşma oluşturulamadı: %s - %s",
|
||||
"copilot_error_reading_stream": "akış okunurken hata oluştu: %w",
|
||||
"copilot_error_stream_request": "akış isteği başarısız oldu: %s - %s",
|
||||
"copilot_failed_create_conversation": "konuşma oluşturulamadı: %w",
|
||||
"copilot_failed_send_message": "mesaj gönderilemedi: %w",
|
||||
"copilot_failed_stream_message": "mesaj akışı sağlanamadı: %w",
|
||||
"copilot_tenant_client_id_required": "kiracı kimliği ve istemci kimliği gereklidir",
|
||||
"copy_to_clipboard": "Panoya Kopyala",
|
||||
"could_not_copy_to_clipboard": "panoya kopyalanamadı: %v",
|
||||
"could_not_create_config_dir": "yapılandırma dizini oluşturulamadı: %w",
|
||||
"could_not_create_env_file": ".env dosyası oluşturulamadı: %w",
|
||||
"could_not_determine_home_dir": "kullanıcı ana dizini belirlenemedi: %w",
|
||||
"could_not_stat_env_file": ".env dosyası bilgileri alınamadı (stat): %w",
|
||||
"custom_notification_command": "Bildirimler için çalıştırılacak özel komut (yerleşik bildirimleri geçersiz kılar)",
|
||||
"custom_patterns_directory_question": "Özel desenler dizininizin yolunu girin",
|
||||
"custom_patterns_label": "Özel Desenler",
|
||||
"custom_patterns_setup_description": "Özel Desenler - Özel desenleriniz için dizini belirleyin",
|
||||
"custom_patterns_warning_create_directory": "Uyarı: Özel desenler dizini %s oluşturulamadı: %v\n",
|
||||
"db_error_loading_env_file": ".env dosyası yüklenirken hata oluştu: %w",
|
||||
"db_error_updating_env_file": ".env dosyası güncellenirken hata oluştu: %w",
|
||||
"defaults_model_context_length_question": "Model bağlam uzunluğunu girin",
|
||||
"defaults_model_question": "Varsayılan modelinizin dizinini veya adını girin",
|
||||
"defaults_setup_description": "Varsayılan Yapay Zeka Sağlayıcısı ve Modeli",
|
||||
"digitalocean_failed_parse_control_plane_url": "DigitalOcean kontrol düzlemi URL'si ayrıştırılamadı: %w",
|
||||
"digitalocean_model_list_unavailable": "DigitalOcean model listesi kullanılamıyor. Modelleri kontrol düzleminden almak için DIGITALOCEAN_TOKEN değerini ayarlayın",
|
||||
"digitalocean_model_list_unavailable_with_error": "DigitalOcean model listesi kullanılamıyor: %w. Modelleri kontrol düzleminden almak için DIGITALOCEAN_TOKEN değerini ayarlayın",
|
||||
"digitalocean_models_request_failed_read_error": "DigitalOcean modelleri isteği %d durumuyla başarısız oldu: %w",
|
||||
"digitalocean_models_request_failed_with_status": "DigitalOcean modelleri isteği %d durumuyla başarısız oldu: %s",
|
||||
"disable_openai_responses_api": "OpenAI Yanıt API'sini devre dışı bırak (varsayılan: false)",
|
||||
"disable_pattern_variable_replacement": "Desen değişkeni değiştirmeyi devre dışı bırak",
|
||||
"enable_web_search_tool": "Desteklenen modeller için web arama aracını etkinleştir (Anthropic, OpenAI, Gemini, Grok)",
|
||||
"end_tag_thinking_sections": "Düşünme bölümleri için bitiş etiketi",
|
||||
"error_creating_audio_file": "ses dosyası oluşturulurken hata oluştu: %v",
|
||||
"error_creating_file": "dosya oluşturulurken hata oluştu: %v",
|
||||
"error_fetching_playlist_videos": "çalma listesi videoları alınırken hata oluştu: %w",
|
||||
"error_parsing_config_file": "yapılandırma dosyası ayrıştırılırken hata oluştu: %w",
|
||||
"error_reading_config_file": "yapılandırma dosyası okunurken hata oluştu: %w",
|
||||
"error_reading_piped_message": "stdin'den yönlendirilen mesaj okunurken hata oluştu: %w",
|
||||
"error_writing_audio_data": "ses verileri dosyaya yazılırken hata oluştu: %v",
|
||||
"error_writing_to_file": "dosyaya yazılırken hata oluştu: %v",
|
||||
"extension_cmd_template_required": "işlem %s için komut şablonu gereklidir",
|
||||
"extension_command_template_label": " Komut Şablonu: %s\n",
|
||||
"extension_config_hash_mismatch": "%s için yapılandırma dosyası hash uyuşmazlığı",
|
||||
"extension_config_path_label": " Yapılandırma Yolu: %s\n\n",
|
||||
"extension_description_label": " Açıklama: %s\n",
|
||||
"extension_empty_command": "biçimlendirme sonrası boş komut",
|
||||
"extension_executable_hash_mismatch": "%s için çalıştırılabilir dosya hash uyuşmazlığı",
|
||||
"extension_executable_label": " Çalıştırılabilir: %s\n",
|
||||
"extension_executable_not_found": "çalıştırılabilir dosya bulunamadı: %w",
|
||||
"extension_executable_required": "çalıştırılabilir dosya yolu gereklidir",
|
||||
"extension_executing_command": "Komut yürütülüyor: %s\n",
|
||||
"extension_execution_failed_err": "yürütme başarısız oldu: %w\nhata: %s",
|
||||
"extension_execution_failed_stderr": "yürütme başarısız oldu: %w\nstderr: %s",
|
||||
"extension_execution_timed_out": "%v sonra yürütme zaman aşımına uğradı",
|
||||
"extension_failed_format_command": "komut biçimlendirilemedi: %w",
|
||||
"extension_failed_get_absolute_path": "mutlak yol alınamadı: %w",
|
||||
"extension_failed_get_extension": "uzantı alınamadı: %w",
|
||||
"extension_failed_get_output_path": "çıktı yolu alınamadı: %w\nhata: %s",
|
||||
"extension_failed_hash_executable": "çalıştırılabilir dosya hash'i alınamadı: %w",
|
||||
"extension_failed_marshal_registry": "uzantı kaydı sıralanamadı: %w",
|
||||
"extension_failed_parse_config": "yapılandırma dosyası ayrıştırılamadı: %w",
|
||||
"extension_failed_parse_registry": "uzantı kaydı ayrıştırılamadı: %w",
|
||||
"extension_failed_read_config": "yapılandırma dosyası okunamadı: %w",
|
||||
"extension_failed_read_output_file": "çıktı dosyası okunamadı: %w",
|
||||
"extension_failed_read_registry": "uzantı kaydı okunamadı: %w",
|
||||
"extension_failed_register": "uzantı kaydedilemedi: %w",
|
||||
"extension_failed_remove": "uzantı kaldırılamadı: %w",
|
||||
"extension_failed_verify_executable": "çalıştırılabilir dosya doğrulanamadı: %w",
|
||||
"extension_file_config_label": " Dosya Yapılandırması:\n",
|
||||
"extension_invalid_config_path": "geçersiz yapılandırma yolu: %w",
|
||||
"extension_invalid_definition": "geçersiz uzantı tanımı: %w",
|
||||
"extension_invalid_timeout": "geçersiz zaman aşımı değeri '%s': '30s' veya '1m' gibi bir süre olmalıdır: %w",
|
||||
"extension_invalid_timeout_format": "geçersiz zaman aşımı biçimi: %w",
|
||||
"extension_name_contains_spaces": "uzantı adı '%s' boşluk içeriyor - adlar boşluk içermemeli",
|
||||
"extension_name_detail_label": "Adı: %s\n",
|
||||
"extension_name_empty": "uzantı adı boş olamaz",
|
||||
"extension_name_label": "Uzantı: %s\n",
|
||||
"extension_name_required": "uzantı adı gereklidir",
|
||||
"extension_no_file_config": "dosya yapılandırması bulunamadı",
|
||||
"extension_no_output_file": "yapılandırmada çıktı dosyası belirtilmemiş",
|
||||
"extension_not_found": "uzantı %s bulunamadı",
|
||||
"extension_operation_not_found": "uzantı %s için işlem %s bulunamadı",
|
||||
"extension_operation_required": "en az bir işlem tanımlanmalıdır",
|
||||
"extension_operations_label": " İşlemler:\n",
|
||||
"extension_registered_success": "Uzantı başarıyla kaydedildi:\n",
|
||||
"extension_registry_not_initialized": "uzantı kaydı başlatılmadı",
|
||||
"extension_status_disabled": " Durum: DEVRE DIŞI - Karma doğrulama başarısız oldu: %v\n",
|
||||
"extension_status_enabled": " Durum: ETKİN\n",
|
||||
"extension_timeout_label": " Zaman Aşımı: %s\n",
|
||||
"extension_type_label": " Tür: %s\n",
|
||||
"extension_type_required": "uzantı türü gerekli",
|
||||
"extension_version_label": " Sürüm: %s\n",
|
||||
"extension_warning_load_registry": "Uyarı: uzantı kaydı yüklenemedi: %v\n",
|
||||
"fabric_command_complete": "Fabric Komutu Tamamlandı",
|
||||
"fabric_command_complete_with_pattern": "Fabric: %s Tamamlandı",
|
||||
"fetch_content_exceeds_limit": "getir: içerik çok büyük: %d baytı aşıyor",
|
||||
"fetch_content_not_utf8": "getir: içerik geçerli UTF-8 metni değil",
|
||||
"fetch_content_null_bytes": "getir: içerik boş baytlar içeriyor",
|
||||
"fetch_content_too_large": "getir: içerik çok büyük: %d bayt (maks %d bayt)",
|
||||
"fetch_error_create_request": "getir: istek oluşturulurken hata: %v",
|
||||
"fetch_error_fetching_url": "getir: URL getirilirken hata: %v",
|
||||
"fetch_error_reading_response": "getir: yanıt okunurken hata: %v",
|
||||
"fetch_http_error": "getir: HTTP hatası: %d - %s",
|
||||
"fetch_unknown_operation": "getir: bilinmeyen işlem %q (desteklenen: get)",
|
||||
"fetch_unsupported_content_type": "getir: desteklenmeyen içerik türü %q - yalnızca metin içeriğine izin verilir",
|
||||
"file_already_exists_choose_different": "dosya %s zaten mevcut. Lütfen farklı bir dosya adı seçin veya mevcut dosyayı kaldırın",
|
||||
"file_already_exists_not_overwriting": "dosya %s zaten mevcut, üzerine yazılmıyor. Mevcut dosyayı yeniden adlandırın veya farklı bir ad seçin",
|
||||
"file_manager_applied_operation": "%s işlemi %s üzerine uygulandı",
|
||||
"file_manager_empty_path": "dosya değişikliği %d için boş yol",
|
||||
"file_manager_failed_create_directory": "failed to create directory %s for file change %d: %w",
|
||||
"file_manager_failed_parse_json": "%s JSON ayrıştırılamadı: %w",
|
||||
"file_manager_failed_write_file": "failed to write file %s for file change %d: %w",
|
||||
"file_manager_file_content_too_large": "dosya değişikliği %d için dosya içeriği çok büyük: %d bayt",
|
||||
"file_manager_invalid_format_no_json_array": "geçersiz %s formatı: JSON dizisi bulunamadı",
|
||||
"file_manager_invalid_format_unbalanced_brackets": "geçersiz %s formatı: dengesiz parantezler",
|
||||
"file_manager_invalid_operation": "dosya değişikliği %d için geçersiz işlem: %s",
|
||||
"file_manager_suspicious_path": "dosya değişikliği %d için şüpheli yol: %s",
|
||||
"gemini_audio_data_too_small": "ses verisi çok küçük: %d bayt, minimum gerekli: %d",
|
||||
"gemini_empty_pcm_data": "boş PCM verisi sağlandı",
|
||||
"gemini_invalid_location_format": "geçersiz arama konumu formatı %q: saat dilimi (örn., 'America/Los_Angeles') veya dil kodu (örn., 'en-US') olmalı",
|
||||
"gemini_invalid_voice": "geçersiz ses '%s'. Geçerli sesler: %v",
|
||||
"gemini_no_audio_data": "TTS modelinden ses verisi alınmadı",
|
||||
"gemini_no_text_for_tts": "TTS oluşturma için metin içeriği bulunamadı",
|
||||
"gemini_pcm_data_too_large": "PCM verisi çok büyük: %d bayt, izin verilen maksimum: %d",
|
||||
"gemini_stream_error": "Hata: %v",
|
||||
"gemini_tts_failed": "TTS oluşturma başarısız oldu: %w",
|
||||
"gemini_unexpected_data_type": "beklenmeyen veri türü: %s, beklenen ses verisi",
|
||||
"gemini_voice_not_found": "ses '%s' bulunamadı",
|
||||
"gemini_wav_data_invalid": "oluşturulan WAV verisi geçersiz: %d bayt, minimum gerekli: %d",
|
||||
"gemini_wav_generation_failed": "WAV dosyası oluşturulamadı: %w",
|
||||
"githelper_failed_clone_repository": "depo klonlanamadı: %w",
|
||||
"githelper_failed_create_dest_directory": "hedef dizin oluşturulamadı: %w",
|
||||
"githelper_failed_create_temp_directory": "geçici dizin oluşturulamadı: %w",
|
||||
"githelper_failed_get_commit": "commit alınamadı: %w",
|
||||
"githelper_failed_get_head": "depo HEAD'i alınamadı: %w",
|
||||
"githelper_failed_get_tree": "ağaç alınamadı: %w",
|
||||
"githelper_failed_git_cli_clone": "git klonlama başarısız oldu: %w: %s",
|
||||
"githelper_failed_git_cli_fallback": "%w; git CLI geri dönüşü de başarısız oldu: %v",
|
||||
"grab_comments_from_youtube": "YouTube videosundaki yorumları al ve sohbete gönder",
|
||||
"grab_transcript_from_youtube": "YouTube videosundaki dökümü al ve sohbete gönder (varsayılan olarak kullanılır).",
|
||||
"grab_transcript_with_timestamps": "Zaman damgalı YouTube videosu dökümünü al ve sohbete gönder",
|
||||
"groups_items_number_out_of_range": "sayı %d aralık dışında",
|
||||
"help_message": "Bu yardım mesajını göster",
|
||||
"help_options_header": "Yardım Seçenekleri:",
|
||||
"html_readability_error": "html okunabilirliği uygulanamadığından orijinal girdiyi kullanın",
|
||||
"i18n_download_failed": "'%s' dili için çeviri indirilemedi: %v",
|
||||
"i18n_load_failed": "çeviri dosyası yüklenemedi: %v",
|
||||
"image_compression_jpeg_webp_only": "görüntü sıkıştırma yalnızca JPEG ve WebP formatlarıyla kullanılabilir, %s ile değil",
|
||||
"image_compression_range_error": "görüntü sıkıştırma 0 ile 100 arasında olmalı, %d alındı",
|
||||
"image_dimensions_help": "Görüntü boyutları: 1024x1024, 1536x1024, 1024x1536, otomatik (varsayılan: otomatik)",
|
||||
"image_file_already_exists": "görüntü dosyası zaten mevcut: %s",
|
||||
"image_parameters_require_image_file": "görüntü parametreleri (--image-size, --image-quality, --image-background, --image-compression) yalnızca --image-file ile kullanılabilir",
|
||||
"image_quality_help": "Görüntü kalitesi: düşük, orta, yüksek, otomatik (varsayılan: otomatik)",
|
||||
"invalid_config_path": "geçersiz yapılandırma yolu: %w",
|
||||
"invalid_image_background": "geçersiz görüntü arka planı '%s'. Desteklenen arka planlar: opak, şeffaf",
|
||||
"invalid_image_file_extension": "geçersiz görüntü dosyası uzantısı '%s'. Desteklenen formatlar: .png, .jpeg, .jpg, .webp",
|
||||
"invalid_image_quality": "geçersiz görüntü kalitesi '%s'. Desteklenen kaliteler: düşük, orta, yüksek, otomatik",
|
||||
"invalid_image_size": "geçersiz görüntü boyutu '%s'. Desteklenen boyutlar: 1024x1024, 1536x1024, 1024x1536, otomatik",
|
||||
"jina_error_creating_request": "istek oluşturulurken hata: %v",
|
||||
"jina_error_reading_response_body": "yanıt gövdesi okunurken hata: %v",
|
||||
"jina_error_sending_request": "istek gönderilirken hata: %v",
|
||||
"jina_label": "Jina AI",
|
||||
"jina_setup_description": "Jina AI Servisi - bir web sayfasını temiz, LLM dostu metin olarak almak için",
|
||||
"language_label": "Dil",
|
||||
"language_output_question": "Varsayılan çıktı dilinizi girin (örneğin: zh_CN)",
|
||||
"language_setup_description": "Dil - Varsayılan Yapay Zeka Sağlayıcısı Çıktı Dili",
|
||||
"list_all_available_models": "Mevcut tüm modelleri listele",
|
||||
"list_all_contexts": "Tüm bağlamları listele",
|
||||
"list_all_patterns": "Tüm desenleri listele",
|
||||
"list_all_registered_extensions": "Tüm kayıtlı uzantıları listele",
|
||||
"list_all_sessions": "Tüm oturumları listele",
|
||||
"list_all_strategies": "Tüm stratejileri listele",
|
||||
"list_all_vendors": "Tüm sağlayıcıları listele",
|
||||
"list_gemini_tts_voices": "Mevcut tüm Gemini TTS seslerini listele",
|
||||
"list_transcription_models": "Mevcut tüm transkripsiyon modellerini listele",
|
||||
"lmstudio_api_url_question": "%v URL'nizi girin (hatırlatıcı olarak, genellikle %v'dir)",
|
||||
"lmstudio_error_reading_response": "yanıt okunurken hata: %w",
|
||||
"lmstudio_failed_create_request": "istek oluşturulamadı: %w",
|
||||
"lmstudio_failed_decode_response": "yanıt kodu çözülemedi: %w",
|
||||
"lmstudio_failed_marshal_payload": "yük ayrıştırılamadı: %w",
|
||||
"lmstudio_failed_send_request": "istek gönderilemedi: %w",
|
||||
"lmstudio_invalid_response_missing_choices": "geçersiz yanıt biçimi: eksik veya boş seçenekler",
|
||||
"lmstudio_invalid_response_missing_content": "geçersiz yanıt biçimi: mesajda eksik veya dize olmayan içerik",
|
||||
"lmstudio_invalid_response_missing_message": "geçersiz yanıt biçimi: ilk seçenekte mesaj eksik",
|
||||
"lmstudio_invalid_response_missing_text": "geçersiz yanıt biçimi: ilk seçenekte eksik veya dize olmayan metin",
|
||||
"lmstudio_no_embeddings_returned": "gömülü ögeler döndürülmedi",
|
||||
"lmstudio_unexpected_status_code": "beklenmeyen durum kodu: %d",
|
||||
"model_context_length_ollama": "Model bağlam uzunluğu (yalnızca ollama'yı etkiler)",
|
||||
"model_for_transcription": "Transkripsiyon için kullanılacak model (sohbet modelinden ayrı)",
|
||||
"no_description_available": "Açıklama mevcut değil",
|
||||
"no_items_found": "Hiçbir %s bulunamadı",
|
||||
"no_notification_system_available": "bildirim sistemi mevcut değil",
|
||||
"notifications_no_provider_available": "bildirim sağlayıcısı mevcut değil",
|
||||
"number_of_latest_patterns": "Listelenecek en son desen sayısı",
|
||||
"ollama_cannot_parse_url": "URL '%s' ayrıştırılamıyor: %v",
|
||||
"ollama_chat_request_failed": "Sohbet isteği başarısız oldu: %v",
|
||||
"ollama_empty_address": "boş adres",
|
||||
"ollama_error_building_chat_url": "/chat URL'si oluşturulurken hata: %v",
|
||||
"ollama_error_creating_chat_request": "/chat isteği oluşturulurken hata: %v",
|
||||
"ollama_error_endpoint": "uç nokta test ediliyor",
|
||||
"ollama_error_getting_chat_body": "/chat gövdesi alınırken hata: %v",
|
||||
"ollama_error_marshalling_body": "gövde ayrıştırılırken hata: %v",
|
||||
"ollama_error_parse_upstream_response": "Hata: yukarı akış yanıtı ayrıştırılamadı",
|
||||
"ollama_error_prefix": "Hata: %s",
|
||||
"ollama_error_reading_body": "gövde okunurken hata: %v",
|
||||
"ollama_error_scanning_body": "gövde taranırken hata: %v",
|
||||
"ollama_error_unmarshalling_body": "gövde ayrıştırılamadı: %v",
|
||||
"ollama_error_writing_response": "yanıt yazılırken hata: %v",
|
||||
"ollama_failed_create_request": "istek oluşturulamadı",
|
||||
"ollama_failed_decode_data_url": "veri URL'si kodu çözülemedi: %v",
|
||||
"ollama_failed_fetch_image": "%s adresinden görsel getirilemedi: %s",
|
||||
"ollama_failed_scan_sse_stream": "SSE yanıt akışı taranamadı: %v",
|
||||
"ollama_failed_unmarshal_fabric_response": "Fabric yanıtı ayrıştırılamadı",
|
||||
"ollama_http_timeout_question": "HTTP zaman aşımını girin (örn. 20d, 60s)",
|
||||
"ollama_invalid_address": "geçersiz adres: %w",
|
||||
"ollama_invalid_address_missing_host": "geçersiz adres: ana bilgisayar eksik",
|
||||
"ollama_invalid_address_missing_hostname": "geçersiz adres: ana bilgisayar adı eksik",
|
||||
"ollama_invalid_address_path_not_allowed": "geçersiz adres: yol bileşeni yalın adreste izin verilmiyor",
|
||||
"ollama_invalid_data_url_format": "geçersiz veri URL biçimi",
|
||||
"ollama_invalid_http_timeout_using_default": "geçersiz HTTP zaman aşımı '%s': %v, varsayılan kullanılıyor",
|
||||
"ollama_invalid_num_ctx_in_request": "istekte geçersiz num_ctx: %v",
|
||||
"ollama_invalid_request_body": "geçersiz istek gövdesi",
|
||||
"ollama_no_content_from_upstream": "yukarı akış Fabric sunucusundan içerik alınamadı",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx, izin verilen maksimum değer olan %d'yi aşıyor",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx bir sayı olmalı, geçersiz tür alındı",
|
||||
"ollama_num_ctx_must_be_finite": "num_ctx sonlu bir sayı olmalı",
|
||||
"ollama_num_ctx_must_be_integer": "num_ctx bir tam sayı olmalı, kesirli kısmı olan ondalık sayı alındı",
|
||||
"ollama_num_ctx_must_be_positive": "num_ctx pozitif olmalı, alındı: %d",
|
||||
"ollama_num_ctx_must_be_valid_number": "num_ctx geçerli bir sayı olmalı",
|
||||
"ollama_num_ctx_must_be_valid_number_got": "num_ctx geçerli bir sayı olmalı, alındı: %s",
|
||||
"ollama_num_ctx_value_out_of_range": "num_ctx değeri aralık dışında",
|
||||
"ollama_num_ctx_value_too_large": "num_ctx değeri çok büyük: %d",
|
||||
"ollama_sse_buffer_limit": "SSE satırı 1MB arabellek sınırını aşıyor - veri satırı çok büyük",
|
||||
"ollama_upstream_non_2xx": "Yukarı akış Fabric sunucusu 2xx olmayan durum kodu %d döndürdü: %s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "Yukarı akış Fabric sunucusu 2xx olmayan durum kodu %d döndürdü ve gövde okunamadı: %v",
|
||||
"ollama_upstream_request_failed": "yukarı akış Fabric sunucusuna ulaşılamadı",
|
||||
"ollama_upstream_returned_status": "yukarı akış Fabric sunucusu %d durum kodunu döndürdü",
|
||||
"ollama_warning_no_content": "Uyarı: yukarı akış Fabric sunucusundan içerik alınamadı",
|
||||
"ollama_warning_parse_variables": "Uyarı: options.variables JSON olarak ayrıştırılamadı: %v",
|
||||
"openai_api_base_url_not_configured": "API temel URL'si %s sağlayıcısı için yapılandırılmamış",
|
||||
"openai_audio_ffmpeg_failed": "ffmpeg başarısız oldu: %v: %s",
|
||||
"openai_audio_ffmpeg_not_found_install": "ffmpeg bulunamadı: lütfen yükleyin",
|
||||
"openai_audio_file_exceeds_limit_enable_split": "Dosya %s 25MB sınırını aşıyor; otomatik bölmeyi etkinleştirmek için --split-media-file kullanın",
|
||||
"openai_audio_file_exceeds_limit_splitting": "Dosya %s boyut sınırından daha büyük... parçalara ayrılıyor...",
|
||||
"openai_audio_model_not_supported_for_transcription": "model '%s' transkripsiyon için desteklenmiyor",
|
||||
"openai_audio_running_ffmpeg_split_chunks": "Sesi %d saniyelik parçalara ayırmak için ffmpeg çalıştırılıyor...",
|
||||
"openai_audio_unable_to_split_acceptable_size_chunks": "dosyayı kabul edilebilir boyutta parçalara ayıramıyor",
|
||||
"openai_audio_unsupported_audio_format": "desteklenmeyen ses formatı '%s'",
|
||||
"openai_audio_using_model_to_transcribe_part": "%s modeli kullanılarak %d. parça yazıya dökülüyor (dosya adı: %s)...",
|
||||
"openai_compatible_unknown_static_model_list": "bilinmeyen statik model listesi: %s",
|
||||
"openai_failed_to_create_models_url": "modeller URL'si oluşturulamadı: %w",
|
||||
"openai_image_failed_to_create_directory": "%s dizini oluşturulamadı: %w",
|
||||
"openai_image_failed_to_decode_image_data": "görsel verisi çözülemedi: %w",
|
||||
"openai_image_failed_to_save_image": "görsel %s konumuna kaydedilemedi: %w",
|
||||
"openai_image_saved_to": "Görsel kaydedildi: %s",
|
||||
"openai_model_no_image_generation": "'%s' modeli görsel oluşturmayı desteklemiyor. Desteklenen modeller: %s",
|
||||
"openai_models_rate_limited": "%s sağlayıcısından modeller alınırken hız sınırı aşıldı; %s saniye sonra tekrar deneyin",
|
||||
"openai_models_response_too_large": "%s sağlayıcısından gelen modeller yanıtı çok büyük (>%d bayt)",
|
||||
"openai_unable_to_parse_models_response": "modeller yanıtı ayrıştırılamadı; ham yanıt: %s",
|
||||
"openai_unexpected_status_code_read_error": "unexpected status code: %d from provider %s (failed to read response body: %v)",
|
||||
"openai_unexpected_status_code_with_body": "unexpected status code: %d from provider %s, response body: %s",
|
||||
"openai_warning_model_no_image_generation": "Uyarı: '%s' modeli görsel oluşturmayı desteklemiyor. Desteklenen modeller: %s. Görsel oluşturmak için -m gpt-5.2 kullanmayı düşünebilirsiniz.\n",
|
||||
"optional_marker": "(isteğe bağlı)",
|
||||
"options_placeholder": "[SEÇENEKLER]",
|
||||
"output_entire_session": "Tüm oturumu (geçici olanı da dahil) çıktı dosyasına yaz",
|
||||
"output_full": "Çıktı: %s",
|
||||
"output_raw_list_shell_completion": "Başlıklar/biçimlendirme olmadan ham listeyi çıkar (kabuk tamamlama için)",
|
||||
"output_to_file": "Dosyaya çıktı al",
|
||||
"output_truncated": "Çıktı: %s...",
|
||||
"output_video_metadata": "Video meta verilerini çıkar",
|
||||
"path_to_yaml_config": "YAML yapılandırma dosyası yolu",
|
||||
"pattern_not_found_list_available": "'%s' deseni bulunamadı. Mevcut desenleri görmek için 'fabric -l' komutunu çalıştırın",
|
||||
"pattern_invalid_name": "geçersiz desen adı: %q",
|
||||
"pattern_not_found_no_patterns": "'%s' deseni bulunamadı.\n\nHiçbir desen yüklü değil! Bunu düzeltmek için:\n • Desenleri yapılandırmak ve indirmek için 'fabric --setup' komutunu çalıştırın\n • Veya desenleri doğrudan indirip güncellemek için 'fabric -U' komutunu çalıştırın",
|
||||
"pattern_variables_help": "Desen değişkenleri için değerler, örn. -v=#role:expert -v=#points:30",
|
||||
"patterns_cloning_repository": "Depo kopyalanıyor %s (yol: %s)...\n",
|
||||
"patterns_debug_included_custom_directory": "📂 Özel dizinden desenler de dahil edildi: %s\n",
|
||||
"patterns_detected_old_path": "🔄 Eski desen yolu 'patterns' algılandı, 'data/patterns' konumuna geçiş deneniyor...",
|
||||
"patterns_download_success": "✅ Desenler başarıyla indirildi ve %s konumuna yüklendi\n",
|
||||
"patterns_downloaded_temp": "%d desen geçici dizine indirildi\n",
|
||||
"patterns_downloading": "Desenler indiriliyor ve %s dolduruluyor...\n",
|
||||
"patterns_error_create_directory": "desen dizini oluşturulamadı: %v",
|
||||
"patterns_error_get_home_directory": "ana dizin alınamadı: %v",
|
||||
"patterns_error_load_from_file": "%s dosyasından desen yüklenemedi: %w",
|
||||
"patterns_error_read_pattern_file": "%s desen dosyası okunamadı: %v",
|
||||
"patterns_error_read_unique_file": "benzersiz desenler dosyası okunamadı. Lütfen --updatepatterns (%s) komutunu çalıştırın",
|
||||
"patterns_error_resolve_file_path": "dosya yolu çözümlenemedi: %v",
|
||||
"patterns_error_save_pattern": "desen kaydedilemedi: %v",
|
||||
"patterns_failed_access_directory": "desenler dizini '%s' erişilemedi: %w",
|
||||
"patterns_failed_create_temp_dir": "geçici dizin oluşturulamadı: %w",
|
||||
"patterns_failed_create_temp_folder": "geçici desenler klasörü oluşturulamadı: %w",
|
||||
"patterns_failed_download_from_git": "desenler git deposundan indirilemedi: %w",
|
||||
"patterns_failed_download_from_repo": "%s konumundan desenler indirilemedi: %w",
|
||||
"patterns_failed_loaded_marker": "yüklü işaretleyici dosyası '%s' oluşturulamadı: %w",
|
||||
"patterns_failed_move_patterns": "desenler yapılandırma dizinine taşınamadı: %w",
|
||||
"patterns_failed_move_test_patterns": "test desenleri geçici klasöre taşınamadı: %w",
|
||||
"patterns_failed_read_directory": "desenler dizini okunamadı: %w",
|
||||
"patterns_failed_read_temp_directory": "geçici desenler dizini okunamadı: %w",
|
||||
"patterns_failed_unique_file": "benzersiz desenler dosyası oluşturulamadı: %w",
|
||||
"patterns_failed_write_unique_file": "benzersiz desenler dosyası yazılamadı: %w",
|
||||
"patterns_found_new_path": "✅ Found %d patterns at new path '%s', updating configuration...\n",
|
||||
"patterns_git_repo_folder_question": "Desenlerin saklandığı Git deposundaki varsayılan klasörü girin",
|
||||
"patterns_git_repo_url_question": "Desenler için varsayılan Git deposu URL'sini girin",
|
||||
"patterns_loader_label": "Desen Yükleyici",
|
||||
"patterns_no_patterns_copied": "hiçbir desen %s konumuna başarıyla kopyalanamadı",
|
||||
"patterns_no_patterns_found_in_directories": "%s ve %s dizinlerinde desen bulunamadı",
|
||||
"patterns_no_patterns_found_in_directory": "%s dizininde desen bulunamadı",
|
||||
"patterns_no_patterns_migration_failed": "%s yolundaki depoda desen bulunamadı ve geçiş başarısız oldu: %w",
|
||||
"patterns_not_found_header": "⚠️ Desen bulunamadı!",
|
||||
"patterns_option_run_setup": "Seçenek 1 (Önerilen): Desenleri indirmek için kurulumu çalıştırın",
|
||||
"patterns_option_run_setup_command": "fabric --setup",
|
||||
"patterns_option_run_update": "Seçenek 2: Desenleri doğrudan indir/güncelle",
|
||||
"patterns_option_run_update_command": "fabric -U",
|
||||
"patterns_preserve_warning": "Uyarı: özel desen '%s' korunamadı: %v\n",
|
||||
"patterns_preserved_custom_pattern": "Korunan özel desen: %s\n",
|
||||
"patterns_required_to_work": "Fabric'in çalışması için desenler gereklidir. Bunu düzeltmek için:",
|
||||
"patterns_saving_updated_configuration": "💾 Güncellenmiş yapılandırma kaydediliyor (yol '%s' konumundan '%s' konumuna değişti)...\n",
|
||||
"patterns_setup_description": "Desenler - Desenleri indirir",
|
||||
"patterns_unable_to_find_or_migrate": "mevcut '%s' yolunda desenler bulunamadı veya yeni yapıya geçilemedi",
|
||||
"patterns_unique_file_created": "📝 %d desen içeren benzersiz desenler dosyası oluşturuldu\n",
|
||||
"patterns_warning_custom_directory": "Uyarı: Özel desenler dizini %s okunamadı: %v\n",
|
||||
"patterns_warning_remove_test_folder": "Uyarı: test geçici klasörü '%s' kaldırılamadı: %v\n",
|
||||
"perplexity_api_key_not_configured": "%s için API anahtarı yapılandırılmadı. %s ortam değişkenini ayarlayın veya %s yapılandırmak için 'fabric --setup' komutunu çalıştırın",
|
||||
"perplexity_api_request_failed": "Perplexity API isteği başarısız oldu: %w",
|
||||
"perplexity_citations_header": "\n\n**Alıntılar:**\n",
|
||||
"perplexity_failed_configure": "Perplexity yapılandırılamadı: %w",
|
||||
"perplexity_streaming_error": "Perplexity akış hatası: %v",
|
||||
"plugin_configured": " ✓",
|
||||
"plugin_enable_bool_question": "%v %v etkinleştirilsin mi (true/false)",
|
||||
"plugin_enter_value": "%v %v değerinizi girin",
|
||||
"plugin_invalid_bool": "geçersiz boolean: %q",
|
||||
"plugin_invalid_boolean_value": "geçersiz boolean değeri: %v",
|
||||
"plugin_not_configured": " ⚠️ YAPILANDIRILMADI",
|
||||
"plugin_question_bool": "%v%v (doğru/yanlış, '%s' için boş bırakın veya değeri kaldırmak için '%v' yazın):",
|
||||
"plugin_question_optional": "%v%v (atlamak için boş bırakın):",
|
||||
"plugin_question_with_default": "%v%v ('%s' için boş bırakın veya değeri kaldırmak için '%v' yazın):",
|
||||
"plugin_registry_could_not_find_vendor": "sağlayıcı bulunamadı",
|
||||
"plugin_registry_error_configuring_custom_patterns": "Özel Desenler yapılandırılırken hata oluştu: %w",
|
||||
"plugin_registry_model_not_available_for_vendor": "%s modeli %s sağlayıcısı için mevcut değil",
|
||||
"plugin_registry_run_setup_select_defaults": "lütfen 'fabric --setup' komutunu çalıştırın ve varsayılan modeli ve sağlayıcıyı seçin",
|
||||
"plugin_setting_not_valid": "%v=%v, geçerli değil",
|
||||
"plugin_setup_configured": "[%v] yapılandırıldı",
|
||||
"plugin_setup_skipped": "[%v] atlandı\n",
|
||||
"prefer_playlist_over_video": "URL'de her iki kimlik de varsa oynatma listesini videoya tercih et",
|
||||
"print_context": "Bağlamı yazdır",
|
||||
"print_current_version": "Mevcut sürümü yazdır",
|
||||
"print_metadata_to_stderr": "Meta verileri (girdi/çıktı jetonları) stderr'e yazdır",
|
||||
"print_pattern_contents": "Adlandırılmış desenin içeriğini terminale yazdır",
|
||||
"print_session": "Oturumu yazdır",
|
||||
"register_new_extension": "Yapılandırma dosyası yolundan yeni bir uzantı kaydet",
|
||||
"remove_registered_extension": "Kayıtlı bir uzantıyı adına göre kaldır",
|
||||
"required_marker": "[gerekli]",
|
||||
"run_setup_for_reconfigurable_parts": "Fabric'in yeniden yapılandırılabilir tüm bölümleri için kurulumu çalıştır",
|
||||
"save_generated_image_to_file": "Oluşturulan görüntüyü belirtilen dosya yoluna kaydet (örn. 'output.png')",
|
||||
"scrape_website_url": "Jina AI kullanarak web sitesi URL'sini Markdown'a dönüştür",
|
||||
"scraping_not_configured": "Kazıma işlevi yapılandırılmadı. Kazımayı etkinleştirmek için lütfen Jina'yı kurun",
|
||||
"search_question_jina": "Jina AI kullanarak soru ara",
|
||||
"seed_for_lmm_generation": "LMM üretimi için kullanılacak başlangıç değeri",
|
||||
"send_desktop_notification": "Komut tamamlandığında masaüstü bildirimi gönder",
|
||||
"serve_fabric_api_ollama_endpoints": "Fabric REST API'sini Ollama uç noktalarıyla sun",
|
||||
"serve_fabric_rest_api": "Fabric REST API'sini sun",
|
||||
"server_api_key_required": "bir API anahtarı olmadan döngüsel olmayan bir adres olan %s üzerinde hizmet vermeyi reddediyor: --api-key veya FABRIC_API_KEY ayarlayın ya da 127.0.0.1:8080 gibi bir döngüsel adres bağlayın",
|
||||
"server_chat_error": "Hata: %v",
|
||||
"server_error_marshaling_response": "yanıt sıralanırken hata oluştu: %v",
|
||||
"server_error_writing_response": "yanıt yazılırken hata oluştu: %v",
|
||||
"server_invalid_request_format": "geçersiz istek formatı: %v",
|
||||
"server_no_api_key_warning": "REST API sunucusu API anahtarı doğrulaması olmadan başlatılıyor. Bu durum güvenlik riskleri oluşturabilir.",
|
||||
"sessions_creating_new": "Yeni oturum oluşturuluyor: %s\n",
|
||||
"set_debug_level": "Hata ayıklama düzeyini ayarla (0=kapalı, 1=temel, 2=ayrıntılı, 3=izleme, 4=tel)",
|
||||
"set_frequency_penalty": "Frekans cezasını ayarla",
|
||||
"set_location_web_search": "Web arama sonuçları için konumu ayarla (örn. 'America/Los_Angeles')",
|
||||
"set_presence_penalty": "Mevcudiyet cezasını ayarla",
|
||||
"set_reasoning_thinking_level": "Akıl yürütme/düşünme düzeyini ayarla (örn. kapalı, düşük, orta, yüksek veya Anthropic veya Google Gemini için sayısal jetonlar)",
|
||||
"set_temperature": "Sıcaklığı ayarla",
|
||||
"set_top_p": "En üst P'yi ayarla",
|
||||
"setup_add_more_providers_later": "'fabric --setup' ile daha sonra ek sağlayıcılar ekleyebileceksiniz",
|
||||
"setup_ai_provider_required": "Fabric'in çalışması için en az bir yapay zeka sağlayıcısına ihtiyacı var.",
|
||||
"setup_available_ai_providers": "Mevcut Yapay Zeka Sağlayıcıları:",
|
||||
"setup_available_plugins": "Mevcut eklentiler:",
|
||||
"setup_complete_header": "✅ Kurulum tamamlandı! Artık Fabric'i kullanabilirsiniz.",
|
||||
"setup_configure_more": "• Daha fazla ayar yapılandır: fabric --setup",
|
||||
"setup_enter_ai_provider_number": "Yapay Zeka Sağlayıcı Numarası",
|
||||
"setup_failed_download_patterns": "desenler indirilemedi: %w",
|
||||
"setup_failed_download_strategies": "stratejiler indirilemedi: %w",
|
||||
"setup_failed_set_defaults": "varsayılan sağlayıcı ve model ayarlanamadı: %w",
|
||||
"setup_invalid_selection": "geçersiz seçim: %s",
|
||||
"setup_list_patterns": "• Mevcut desenleri listele: fabric -l",
|
||||
"setup_next_steps": "Sonraki adımlar:",
|
||||
"setup_no_ai_provider_selected": "hiçbir yapay zeka sağlayıcısı seçilmedi - en az biri gerekli",
|
||||
"setup_optional_configuration_header": "━━━ İSTEĞE BAĞLI YAPILANDIRMA ━━━\n\nİsteğe Bağlı Araçlar",
|
||||
"setup_plugin_number": "Eklenti Numarası",
|
||||
"setup_plugin_prompt": "Kurulacak eklentinin numarasını girin",
|
||||
"setup_required_configuration_header": "━━━ GEREKLİ YAPILANDIRMA ━━━\n\nYapay Zeka Sağlayıcıları [en az biri gerekli]",
|
||||
"setup_required_tools": "Gerekli Araçlar",
|
||||
"setup_step_configure_ai_provider": "🤖 Adım 3: Bir yapay zeka sağlayıcısı yapılandır",
|
||||
"setup_step_downloading_patterns": "📥 Adım 1: Desenler indiriliyor (Fabric'in çalışması için gerekli)...",
|
||||
"setup_step_downloading_strategies": "📥 Adım 2: Stratejiler indiriliyor (Fabric'in çalışması için gerekli)...",
|
||||
"setup_step_setting_defaults": "⚙️ Adım 4: Varsayılan sağlayıcı ve model ayarlanıyor...",
|
||||
"setup_try_pattern": "• Bir desen dene: echo 'metniniz' | fabric --pattern summarize",
|
||||
"setup_validation_ai_provider_configured": "✓ Yapay Zeka Sağlayıcısı yapılandırıldı",
|
||||
"setup_validation_ai_provider_missing": "✗ Yapay Zeka Sağlayıcısı yapılandırılmadı - Fabric'in çalışması için gerekli",
|
||||
"setup_validation_complete": "✓ Tüm gerekli bileşenler yapılandırıldı!",
|
||||
"setup_validation_defaults_configured": "✓ Varsayılan sağlayıcı/model ayarlandı: %s/%s",
|
||||
"setup_validation_defaults_missing": "✗ Varsayılan sağlayıcı/model ayarlanmadı - Fabric'in çalışması için gerekli",
|
||||
"setup_validation_header": "Yapılandırma Durumu:",
|
||||
"setup_validation_incomplete_help": "Eksik öğeleri yapılandırmak için 'fabric --setup' komutunu tekrar çalıştırın,\nveya desenleri ve stratejileri indirmek için 'fabric -U' komutunu çalıştırın.",
|
||||
"setup_validation_incomplete_warning": "⚠️ Kurulum tamamlanmadı! Gerekli bileşenler eksik.",
|
||||
"setup_validation_patterns_configured": "✓ Desenler indirildi",
|
||||
"setup_validation_patterns_missing": "✗ Desenler bulunamadı - Fabric'in çalışması için gerekli",
|
||||
"setup_validation_strategies_configured": "✓ Stratejiler indirildi",
|
||||
"setup_validation_strategies_missing": "✗ Stratejiler bulunamadı - Fabric'in çalışması için gerekli",
|
||||
"setup_welcome_header": "🎉 Fabric'e hoş geldiniz! Kurulumunuzu yapalım.",
|
||||
"show_dry_run": "Modele gerçekten göndermeden, ne gönderileceğini göster.",
|
||||
"specify_language_code": "Sohbet için Dil Kodunu belirtin, örn. -g=en -g=zh -g=pt-BR -g=pt-PT",
|
||||
"specify_vendor_for_model": "Seçilen model için sağlayıcıyı belirtin (örn., -V \"LM Studio\" -m openai/gpt-oss-20b)",
|
||||
"split_media_files_ffmpeg": "25MB'tan büyük ses/video dosyalarını ffmpeg kullanarak ayırın",
|
||||
"spotify_api_request_failed": "API isteği başarısız oldu: durum %d, gövde: %s",
|
||||
"spotify_audio_preview_label": "**Ses Önizlemesi**: %s",
|
||||
"spotify_client_id_question": "Spotify İSTEMCİ KİMLİĞİNİZİ girin",
|
||||
"spotify_client_secret_question": "Spotify İSTEMCİ SIRRINIZI girin",
|
||||
"spotify_description_header": "## Açıklama",
|
||||
"spotify_duration_label": "**Süre**: %d dakika",
|
||||
"spotify_episode_header": "# Spotify Bölümü",
|
||||
"spotify_error_getting_metadata": "Spotify meta verileri alınırken hata oluştu: %v",
|
||||
"spotify_explicit_label": "**Açık İçerik**: %v",
|
||||
"spotify_failed_create_request": "istek oluşturulamadı: %w",
|
||||
"spotify_failed_create_token_request": "jeton isteği oluşturulamadı: %w",
|
||||
"spotify_failed_decode_token_response": "jeton yanıtı çözümlenemedi: %w",
|
||||
"spotify_failed_execute_request": "istek yürütülemedi: %w",
|
||||
"spotify_failed_get_access_token": "erişim jetonu alınamadı: durum %d, gövde: %s",
|
||||
"spotify_failed_get_show_episodes": "program bölümleri alınamadı: %w",
|
||||
"spotify_failed_parse_episode_metadata": "bölüm meta verileri ayrıştırılamadı: %w",
|
||||
"spotify_failed_parse_episodes": "bölümler ayrıştırılamadı: %w",
|
||||
"spotify_failed_parse_search_results": "arama sonuçları ayrıştırılamadı: %w",
|
||||
"spotify_failed_parse_show_metadata": "program meta verileri ayrıştırılamadı: %w",
|
||||
"spotify_failed_read_response_body": "yanıt gövdesi okunamadı: %w",
|
||||
"spotify_failed_request_access_token": "erişim jetonu isteği başarısız oldu: %w",
|
||||
"spotify_invalid_url": "geçersiz Spotify URL'si, program veya bölüm kimliği alınamıyor: '%s'",
|
||||
"spotify_label": "Spotify",
|
||||
"spotify_language_field_label": "**Dil**: %s",
|
||||
"spotify_languages_label": "**Diller**: %s",
|
||||
"spotify_media_type_label": "**Medya Türü**: %s",
|
||||
"spotify_no_episode_found": "bu kimliğe sahip bölüm bulunamadı: %s",
|
||||
"spotify_no_show_found": "bu kimliğe sahip program bulunamadı: %s",
|
||||
"spotify_not_configured": "Spotify yapılandırılmamış, lütfen kurulum prosedürünü çalıştırın",
|
||||
"spotify_publisher_label": "**Yayıncı**: %s",
|
||||
"spotify_release_date_label": "**Yayın Tarihi**: %s",
|
||||
"spotify_search_description_label": "- **Açıklama**: %s",
|
||||
"spotify_search_episodes_label": "- **Bölümler**: %d",
|
||||
"spotify_search_failed": "arama başarısız oldu: %w",
|
||||
"spotify_search_publisher_label": "- **Yayıncı**: %s",
|
||||
"spotify_search_results_header": "# Spotify Arama Sonuçları",
|
||||
"spotify_search_url_label": "- **URL**: %s",
|
||||
"spotify_setup_description": "Spotify - Spotify'dan podcast/program meta verilerini almak için",
|
||||
"spotify_show_header": "# Spotify Podcast/Programı",
|
||||
"spotify_show_name_label": "**Program**: %s",
|
||||
"spotify_title_label": "**Başlık**: %s",
|
||||
"spotify_total_episodes_label": "**Toplam Bölüm Sayısı**: %d",
|
||||
"spotify_url_help": "Meta verileri almak ve sohbete göndermek için Spotify podcast veya bölüm URL'si",
|
||||
"spotify_url_label": "**URL**: %s",
|
||||
"start_tag_thinking_sections": "Düşünme bölümleri için başlangıç etiketi",
|
||||
"storage_error_delete": "%s silinemedi: %v",
|
||||
"storage_error_load": "%s yüklenemedi: %v",
|
||||
"storage_error_marshal": "%s serileştirilemedi: %s",
|
||||
"storage_error_read_directory": "dizinden öğeler okunamadı: %v",
|
||||
"storage_error_rename": "%s, %s olarak yeniden adlandırılamadı: %v",
|
||||
"storage_error_resolve_directory": "dizin yolu çözümlenemedi: %v",
|
||||
"storage_error_save": "%s kaydedilemedi: %v",
|
||||
"storage_error_stat_entry": "%s girdisi hakkında bilgi alınamadı: %v",
|
||||
"storage_error_unmarshal": "%s seri durumdan çıkarılamadı: %s",
|
||||
"storage_invalid_name": "geçersiz ad: %q",
|
||||
"strategies_available_header": "Mevcut Stratejiler:",
|
||||
"strategies_cloning_repository": "Depo klonlanıyor %s (yol: %s)...\n",
|
||||
"strategies_download_success": "✅ Stratejiler başarıyla indirildi ve %s konumuna kuruldu\n",
|
||||
"strategies_downloaded_count": "%d strateji indirildi\n",
|
||||
"strategies_downloading": "Stratejiler indiriliyor ve %s dolduruluyor...\n",
|
||||
"strategies_failed_create_directory": "stratejiler dizini oluşturulamadı: %w",
|
||||
"strategies_failed_download": "stratejiler indirilemedi: %w",
|
||||
"strategies_git_repo_folder_question": "Stratejilerin saklandığı Git deposundaki varsayılan klasörü girin",
|
||||
"strategies_git_repo_url_question": "Stratejiler için varsayılan Git deposu URL'sini girin",
|
||||
"strategies_home_dir_error": "ana dizin alınamadı: %v",
|
||||
"strategies_home_dir_fallback": "ana dizin alınamadı: %v, bunun yerine mevcut dizin kullanılıyor",
|
||||
"strategies_label": "İstem Stratejileri",
|
||||
"strategies_none_found": "strateji bulunamadı. Stratejileri indirmek için lütfen 'fabric --setup' komutunu çalıştırın",
|
||||
"strategies_setup_description": "Stratejiler - İstem Stratejilerini (düşünce zinciri gibi) indirir",
|
||||
"strategy_not_found": "desen %s bulunamadı. Lütfen listeyi görmek için 'fabric --liststrategies' komutunu çalıştırın.",
|
||||
"strategy_path_traversal": "desen adı %q desen dizininin dışına çözümleniyor",
|
||||
"stream_help": "Akış",
|
||||
"suppress_thinking_tags": "Düşünme etiketleri arasına alınmış metni gizle",
|
||||
"template_datetime_error_invalid_number": "göreceli zamanda geçersiz sayı: %q",
|
||||
"template_datetime_error_invalid_relative_format": "geçersiz göreceli zaman biçimi",
|
||||
"template_datetime_error_invalid_unit": "geçersiz zaman birimi: %q",
|
||||
"template_datetime_error_relative_requires_value": "göreceli zaman bir değer gerektirir",
|
||||
"template_datetime_error_unknown_operation": "tarihsaat: bilinmeyen işlem %q",
|
||||
"template_extension_error": "uzantı %s hatası: %v",
|
||||
"template_file_error_expand_home_dir": "dosya: ana dizin genişletilemedi: %v",
|
||||
"template_file_error_invalid_line_count": "dosya: geçersiz satır sayısı %q",
|
||||
"template_file_error_line_count_positive": "dosya: satır sayısı pozitif olmalıdır",
|
||||
"template_file_error_open_file": "dosya: açılamadı: %v",
|
||||
"template_file_error_path_contains_parent_ref": "dosya: yol '..' içeremez",
|
||||
"template_file_error_read_file": "dosya: okunamadı: %v",
|
||||
"template_file_error_scanner_read": "dosya: okuma hatası: %v",
|
||||
"template_file_error_size_exceeds_limit": "dosya: boyut %d, %d baytlık sınırı aşıyor",
|
||||
"template_file_error_stat_file": "dosya: dosya durumu alınamadı: %v",
|
||||
"template_file_error_stat_open_file": "dosya: durum alınamadı: %v",
|
||||
"template_file_error_tail_requires_path_lines": "dosya: tail, yol|satır biçimini gerektirir",
|
||||
"template_file_error_unknown_operation": "dosya: bilinmeyen işlem %q (desteklenenler: read, tail, exists, size, modified)",
|
||||
"template_file_log_cleaned_path": "Dosya: temizlenmiş yol %q",
|
||||
"template_file_log_exists_for_path": "Dosya: var=%v, yol %q için",
|
||||
"template_file_log_modified_for_path": "Dosya: değiştirildi=%q, yol %q için",
|
||||
"template_file_log_operation_value": "Dosya: işlem=%q değer=%q",
|
||||
"template_file_log_read_bytes": "Dosya: %d bayt okundu",
|
||||
"template_file_log_read_total_return_last": "Dosya: toplam %d satır okundu, son %d döndürülüyor",
|
||||
"template_file_log_reading_last_lines": "File: reading last %d lines from %q",
|
||||
"template_file_log_size_for_path": "Dosya: boyut=%d, yol %q için",
|
||||
"template_file_log_tail_returning_lines": "Dosya: tail %d satır döndürüyor",
|
||||
"template_file_log_validating_path": "Dosya: yol doğrulanıyor %q",
|
||||
"template_hash_open_file": "dosya açılamadı: %w",
|
||||
"template_hash_read_file": "dosya okunamadı: %w",
|
||||
"template_missing_required_variable": "gerekli değişken eksik: %s",
|
||||
"template_plugin_error": "eklenti %s hatası: %v",
|
||||
"template_processing_stuck": "şablon işleme takıldı - potansiyel sonsuz döngü",
|
||||
"template_sys_error_env_requires_var": "ortam: değişken adı gerekli",
|
||||
"template_sys_error_home": "ana dizin alınamadı: %v",
|
||||
"template_sys_error_hostname": "ana bilgisayar adı alınamadı: %v",
|
||||
"template_sys_error_pwd": "çalışma dizini alınamadı: %v",
|
||||
"template_sys_error_unknown_operation": "sistem: bilinmeyen işlem %q",
|
||||
"template_sys_error_user": "mevcut kullanıcı alınamadı: %v",
|
||||
"template_text_empty_input": "metin: %q işlemi için boş giriş",
|
||||
"template_text_unknown_operation": "metin: bilinmeyen metin işlemi %q (desteklenenler: upper, lower, title, trim)",
|
||||
"template_unknown_plugin_namespace": "bilinmeyen eklenti ad alanı: %s",
|
||||
"template_utils_failed_get_absolute_path": "mutlak yol alınamadı: %w",
|
||||
"template_utils_failed_get_home_dir": "kullanıcı ana dizini alınamadı: %w",
|
||||
"template_utils_path_not_exist": "yol mevcut değil: %w",
|
||||
"transcription_model_required": "transkripsiyon modeli gereklidir (--transcribe-model kullanın)",
|
||||
"transparent_background_png_webp_only": "şeffaf arka plan yalnızca PNG ve WebP biçimleriyle kullanılabilir, %s ile değil",
|
||||
"tts_audio_generated_successfully": "TTS sesi başarıyla oluşturuldu ve şuraya kaydedildi: %s\n",
|
||||
"tts_model_requires_audio_output": "TTS modeli '%s' ses çıkışı gerektirir. Lütfen -o bayrağıyla bir ses çıkış dosyası belirtin (örn: -o output.wav)",
|
||||
"tts_voice_name": "Desteklenen modeller için TTS ses adı (örn: Kore, Charon, Puck)",
|
||||
"unsupported_conversion": "desteklenmeyen dönüştürme: %v öğesinden %v öğesine",
|
||||
"update_patterns": "Desenleri Güncelle",
|
||||
"usage_header": "Kullanım:",
|
||||
"use_model_defaults_raw_help": "Modelin varsayılanlarını sohbet seçeneklerini (temperature, top_p vb.) göndermeden kullanın. Yalnızca OpenAI uyumlu sağlayıcıları etkiler. Anthropic modelleri, modele özgü gereksinimlere uymak için her zaman akıllı parametre seçimi kullanır.",
|
||||
"util_error_accessing_config_path": "varsayılan yapılandırma yoluna erişimde hata: %w",
|
||||
"util_error_determine_home_directory": "kullanıcı ana dizini belirlenemedi: %w",
|
||||
"util_error_get_absolute_path": "mutlak yol alınamadı",
|
||||
"util_error_path_is_empty": "yol boş",
|
||||
"util_error_resolve_home_directory": "ana dizin çözümlenemedi",
|
||||
"util_error_resolve_symlinks": "sembolik bağlantılar çözümlenemedi: %w",
|
||||
"vendor_no_transcription_support": "sağlayıcı %s ses transkripsiyonunu desteklemiyor",
|
||||
"vendor_not_configured": "sağlayıcı %s yapılandırılmadı",
|
||||
"vendor_not_found": "sağlayıcı %s bulunamadı",
|
||||
"vendors_no_ai_vendors_configured_read_models": "modelleri okumak için yapılandırılmış hiçbir yapay zeka sağlayıcısı yok",
|
||||
"vertexai_client_not_initialized": "VertexAI istemcisi başlatılmadı",
|
||||
"vertexai_error_api_status": "API %d durumunu döndürdü: %s",
|
||||
"vertexai_error_create_request": "istek oluşturulamadı: %w",
|
||||
"vertexai_error_parse_response": "yanıt ayrıştırılamadı: %w",
|
||||
"vertexai_error_read_response": "yanıt okunamadı: %w",
|
||||
"vertexai_error_request_failed": "istek başarısız oldu: %w",
|
||||
"vertexai_error_response_too_large": "yanıt çok büyük (>%d bayt)",
|
||||
"vertexai_failed_gemini_client": "Gemini istemcisi oluşturulamadı: %w",
|
||||
"vertexai_failed_google_credentials": "Google kimlik bilgileri alınamadı (ADC'nin yapılandırıldığından emin olun): %w",
|
||||
"vertexai_no_content_in_response": "yanıtta içerik yok",
|
||||
"vertexai_no_conversational_models": "konuşma modeli bulunamadı",
|
||||
"vertexai_no_models_found": "hiçbir yayıncıdan model bulunamadı",
|
||||
"vertexai_no_valid_messages": "gönderilecek geçerli mesaj yok",
|
||||
"vertexai_stream_error": "Hata: %v",
|
||||
"wipe_context": "Bağlamı sil",
|
||||
"wipe_session": "Oturumu sil",
|
||||
"youtube_api_key_required": "Yorumlar ve meta veriler için YouTube API anahtarı gerekli. Yapılandırmak için 'fabric --setup' komutunu çalıştırın.",
|
||||
"youtube_auth_required_bot_detection": "YouTube kimlik doğrulama gerektiriyor (bot algılama). BROWSER'ın chrome, firefox, brave vb. olduğu durumlarda --yt-dlp-args='--cookies-from-browser BROWSER' kullanın.",
|
||||
"youtube_empty_seconds_string": "boş saniye dizesi",
|
||||
"youtube_error_getting_comments": "yorumlar alınırken hata oluştu: %v",
|
||||
"youtube_error_getting_metadata": "video meta verileri alınırken hata oluştu: %v",
|
||||
"youtube_error_getting_video_details": "video detayları alınırken hata oluştu: %v",
|
||||
"youtube_error_parsing_duration": "video süresi ayrıştırılırken hata oluştu: %v",
|
||||
"youtube_error_saving_csv": "videolar CSV'ye kaydedilirken hata oluştu: %v",
|
||||
"youtube_extract_visual_data_help": "OCR ve FFmpeg kullanarak videodan görsel veri çıkarın",
|
||||
"youtube_failed_create_temp_dir": "geçici dizin oluşturulamadı: %v",
|
||||
"youtube_failed_fetch_comments": "yorumlar alınamadı: %v",
|
||||
"youtube_failed_get_stream_url": "yt-dlp aracılığıyla akış URL'si alınamadı: %v",
|
||||
"youtube_failed_parse_http_stream_url": "yt-dlp çıktısından geçerli HTTP akış URL'si ayrıştırılamadı",
|
||||
"youtube_failed_walk_directory": "dizin gezilemedi: %v",
|
||||
"youtube_ffmpeg_frame_extraction_failed": "ffmpeg kare çıkarma başarısız oldu: %v, çıktı: %s",
|
||||
"youtube_ffmpeg_required_visual_extraction": "görsel çıkarma için ffmpeg gerekli ancak PATH'te bulunamadı",
|
||||
"youtube_invalid_duration_string": "geçersiz süre dizesi: %s",
|
||||
"youtube_invalid_seconds_format": "geçersiz saniye biçimi %q: %w",
|
||||
"youtube_invalid_timestamp_format": "geçersiz zaman damgası biçimi: %s",
|
||||
"youtube_invalid_url": "geçersiz YouTube URL'si, video veya çalma listesi kimliği alınamıyor: '%s'",
|
||||
"youtube_invalid_ytdlp_arguments": "geçersiz yt-dlp argümanları: %v",
|
||||
"youtube_label": "YouTube",
|
||||
"youtube_no_clear_text_visual_frames": "video görsel karelerinde net metin bulunamadı",
|
||||
"youtube_no_transcript_content": "VTT dosyasında transkript içeriği bulunamadı",
|
||||
"youtube_no_url_provided": "YouTube URL'si sağlanmadı",
|
||||
"youtube_no_video_found_with_id": "ID'si %s olan video bulunamadı",
|
||||
"youtube_no_video_id_found": "URL'de video kimliği bulunamadı",
|
||||
"youtube_no_vtt_files_found": "dizinde VTT dosyası bulunamadı",
|
||||
"youtube_not_configured": "YouTube yapılandırılmamış, lütfen kurulum prosedürünü çalıştırın",
|
||||
"youtube_playlist_header": "Çalma Listesi: %s",
|
||||
"youtube_playlist_saved_to": "Çalma listesi %s konumuna kaydedildi",
|
||||
"youtube_rate_limit_exceeded": "YouTube hız sınırı aşıldı. Daha sonra tekrar deneyin veya istekleri yavaşlatmak için '--sleep-requests 1' gibi farklı yt-dlp argümanları kullanın.",
|
||||
"youtube_setup_description": "YouTube - video transkriptlerini (yt-dlp aracılığıyla) ve yorumları/meta verilerini (YouTube API aracılığıyla) almak için",
|
||||
"youtube_tesseract_frame_failed": "tesseract %d. karede başarısız oldu: %v, stderr: %s",
|
||||
"youtube_tesseract_required_visual_extraction": "görsel çıkarma için tesseract gerekli ancak PATH'te bulunamadı",
|
||||
"youtube_url_help": "Transkripti ve yorumları almak, sohbete göndermek veya konsola yazdırmak ve çıktı dosyasına kaydetmek için YouTube video veya çalma listesi \"URL\"si",
|
||||
"youtube_url_is_playlist_not_video": "URL bir çalma listesi, video değil",
|
||||
"youtube_video_id_title_header": "Video Kimliği: Başlık",
|
||||
"youtube_visual_fps_help": "Sahne algılama kullanmak yerine saniye başına belirli sayıda kare çıkarın",
|
||||
"youtube_visual_frame_cue": "[GÖRSEL KARE İPUCU]",
|
||||
"youtube_visual_sensitivity_help": "FFmpeg sahne algılama toleransı (0.0 - 1.0)",
|
||||
"youtube_ytdlp_not_found": "yt-dlp PATH'te bulunamadı. YouTube transkript işlevselliğini kullanmak için lütfen yt-dlp'yi kurun",
|
||||
"youtube_ytdlp_required_visual_extraction": "görsel çıkarma için yt-dlp gerekli ancak PATH'te bulunamadı",
|
||||
"youtube_ytdlp_stderr_error": "yt-dlp stderr okunurken hata oluştu"
|
||||
}
|
||||
|
|
@ -110,7 +110,9 @@
|
|||
"choose_pattern_from_available": "从可用模式中选择一个模式",
|
||||
"choose_session_from_available": "从可用会话中选择一个会话",
|
||||
"choose_strategy_from_available": "从可用策略中选择一个策略",
|
||||
"codex_api_base_url_question": "请输入您的 Codex API 基础 URL",
|
||||
"codex_auth_base_url_invalid": "Codex 认证基础 URL 无效:%w",
|
||||
"codex_auth_base_url_question": "请输入您的 Codex OAuth 基础 URL",
|
||||
"codex_browser_open_fallback": "如果浏览器未打开,请导航到此 URL 进行身份验证:",
|
||||
"codex_decode_models_response_failed": "解码 Codex 模型响应失败:%w",
|
||||
"codex_decode_refresh_response_failed": "解码刷新的 Codex 令牌响应失败:%w",
|
||||
|
|
@ -133,13 +135,16 @@
|
|||
"codex_oauth_random_state_failed": "生成安全随机 OAuth 状态失败:%w",
|
||||
"codex_oauth_server_start_failed": "启动本地 OAuth 回调服务器失败:%w",
|
||||
"codex_oauth_state_mismatch": "状态不匹配",
|
||||
"codex_provider_error": "Codex 供应商错误(状态 %d):%s",
|
||||
"codex_refresh_failed_status": "刷新 Codex 登录失败(状态 %d)",
|
||||
"codex_refresh_login_failed": "刷新 Codex 登录失败:%w",
|
||||
"codex_refresh_token_required": "需要 Codex 刷新令牌。请重新运行 'fabric --setup'。",
|
||||
"codex_replay_body_unavailable": "请求正文无法重放用于 Codex 重新认证重试",
|
||||
"codex_request_failed": "Codex 请求失败:%w",
|
||||
"codex_request_failed_status": "Codex 请求失败,状态 %d",
|
||||
"codex_starting_browser_login": "正在启动基于浏览器的 OpenAI 登录以连接 Codex。",
|
||||
"codex_token_exchange_failed": "Codex 令牌交换失败:%w",
|
||||
"codex_token_persist_failed": "无法保存 Codex 登录:%w",
|
||||
"codex_token_refresh_missing_access_token": "Codex 令牌刷新未返回访问令牌。",
|
||||
"codex_usage_limit_reached": "已达到 Codex 使用限制",
|
||||
"command_completed_successfully": "命令执行成功",
|
||||
|
|
@ -168,6 +173,7 @@
|
|||
"custom_patterns_setup_description": "自定义模式 - 设置您的自定义模式目录",
|
||||
"custom_patterns_warning_create_directory": "警告:无法创建自定义模式目录 %s:%v\n",
|
||||
"db_error_loading_env_file": "加载 .env 文件错误:%w",
|
||||
"db_error_updating_env_file": "更新 .env 文件错误:%w",
|
||||
"defaults_model_context_length_question": "请输入模型上下文长度",
|
||||
"defaults_model_question": "请输入您的默认模型的索引或名称",
|
||||
"defaults_setup_description": "默认 AI 提供商和模型",
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
"digitalocean_models_request_failed_with_status": "DigitalOcean 模型请求失败,状态码 %d:%s",
|
||||
"disable_openai_responses_api": "禁用 OpenAI 响应 API(默认:false)",
|
||||
"disable_pattern_variable_replacement": "禁用模式变量替换",
|
||||
"enable_web_search_tool": "为支持的模型启用网络搜索工具(Anthropic、OpenAI、Gemini)",
|
||||
"enable_web_search_tool": "为支持的模型启用网络搜索工具(Anthropic、OpenAI、Gemini、Grok)",
|
||||
"end_tag_thinking_sections": "思考部分的结束标签",
|
||||
"error_creating_audio_file": "创建音频文件时出错:%v",
|
||||
"error_creating_file": "创建文件时出错:%v",
|
||||
|
|
@ -369,6 +375,7 @@
|
|||
"ollama_invalid_data_url_format": "无效的数据 URL 格式",
|
||||
"ollama_invalid_http_timeout_using_default": "无效的 HTTP 超时时间 '%s':%v,使用默认值",
|
||||
"ollama_invalid_num_ctx_in_request": "请求中的 num_ctx 无效:%v",
|
||||
"ollama_invalid_request_body": "无效的请求正文",
|
||||
"ollama_no_content_from_upstream": "未从上游 Fabric 服务器收到内容",
|
||||
"ollama_num_ctx_exceeds_maximum": "num_ctx 超过允许的最大值 %d",
|
||||
"ollama_num_ctx_invalid_type": "num_ctx 必须是数字,收到无效类型",
|
||||
|
|
@ -382,6 +389,7 @@
|
|||
"ollama_sse_buffer_limit": "SSE 行超过 1MB 缓冲区限制 - 数据行过大",
|
||||
"ollama_upstream_non_2xx": "上游 Fabric 服务器返回非 2xx 状态 %d:%s",
|
||||
"ollama_upstream_non_2xx_body_unreadable": "上游 Fabric 服务器返回非 2xx 状态 %d 且无法读取正文:%v",
|
||||
"ollama_upstream_request_failed": "无法连接上游 Fabric 服务器",
|
||||
"ollama_upstream_returned_status": "上游 Fabric 服务器返回状态 %d",
|
||||
"ollama_warning_no_content": "警告:未从上游 Fabric 服务器收到内容",
|
||||
"ollama_warning_parse_variables": "警告:无法将 options.variables 解析为 JSON:%v",
|
||||
|
|
@ -492,6 +500,8 @@
|
|||
"prefer_playlist_over_video": "如果 URL 中同时存在两个 ID,则优先选择播放列表而不是视频",
|
||||
"print_context": "打印上下文",
|
||||
"print_current_version": "打印当前版本",
|
||||
"print_metadata_to_stderr": "将元数据(输入/输出令牌)打印到 stderr",
|
||||
"print_pattern_contents": "将指定模式的内容打印到终端",
|
||||
"print_session": "打印会话",
|
||||
"register_new_extension": "从配置文件路径注册新扩展",
|
||||
"remove_registered_extension": "按名称删除已注册的扩展",
|
||||
|
|
@ -505,10 +515,12 @@
|
|||
"send_desktop_notification": "命令完成时发送桌面通知",
|
||||
"serve_fabric_api_ollama_endpoints": "提供带有 ollama 端点的 Fabric REST API 服务",
|
||||
"serve_fabric_rest_api": "提供 Fabric REST API 服务",
|
||||
"server_api_key_required": "拒绝在非回环地址 %s 上提供服务(未配置 API 密钥):请设置 --api-key 或 FABRIC_API_KEY,或绑定回环地址(如 127.0.0.1:8080)",
|
||||
"server_chat_error": "错误:%v",
|
||||
"server_error_marshaling_response": "序列化响应错误:%v",
|
||||
"server_error_writing_response": "写入响应错误:%v",
|
||||
"server_invalid_request_format": "无效的请求格式:%v",
|
||||
"server_no_api_key_warning": "正在启动 REST API 服务器,未启用 API 密钥身份验证。这可能带来安全风险。",
|
||||
"sessions_creating_new": "正在创建新会话:%s\n",
|
||||
"set_debug_level": "设置调试级别(0=关闭,1=基本,2=详细,3=跟踪,4=wire)",
|
||||
"set_frequency_penalty": "设置频率惩罚",
|
||||
|
|
@ -600,6 +612,7 @@
|
|||
"spotify_show_name_label": "**节目**:%s",
|
||||
"spotify_title_label": "**标题**:%s",
|
||||
"spotify_total_episodes_label": "**总剧集数**:%d",
|
||||
"spotify_url_help": "Spotify 播客或单集 URL,用于获取元数据并发送到聊天",
|
||||
"spotify_url_label": "**URL**:%s",
|
||||
"start_tag_thinking_sections": "思考部分的开始标签",
|
||||
"storage_error_delete": "无法删除 %s:%v",
|
||||
|
|
@ -611,6 +624,7 @@
|
|||
"storage_error_save": "无法保存 %s:%v",
|
||||
"storage_error_stat_entry": "无法获取条目 %s 的信息:%v",
|
||||
"storage_error_unmarshal": "无法反序列化 %s:%s",
|
||||
"storage_invalid_name": "无效的名称:%q",
|
||||
"strategies_available_header": "可用的策略:",
|
||||
"strategies_cloning_repository": "正在克隆仓库 %s(至路径:%s)...\\n",
|
||||
"strategies_download_success": "✅ 已成功下载并安装策略到 %s\\n",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,45 @@ type authTransport struct {
|
|||
}
|
||||
|
||||
func (c *Client) ensureAccessToken(ctx context.Context, forceRefresh bool) (string, string, error) {
|
||||
// Fast path: a valid in-memory token needs no refresh, so avoid the store
|
||||
// lock and disk reload. This also keeps Setup's fresh OAuth tokens from
|
||||
// being overwritten by the stale values still on disk before SaveEnvFile.
|
||||
if !forceRefresh {
|
||||
if access, account, ok := c.currentToken(); ok {
|
||||
return access, account, nil
|
||||
}
|
||||
}
|
||||
if c.WithStoreLock == nil {
|
||||
return c.ensureAccessTokenLocked(ctx, forceRefresh)
|
||||
}
|
||||
var access, account string
|
||||
err := c.WithStoreLock(func() error {
|
||||
var inner error
|
||||
access, account, inner = c.ensureAccessTokenLocked(ctx, forceRefresh)
|
||||
return inner
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return access, account, nil
|
||||
}
|
||||
|
||||
// currentToken returns the in-memory token when it is present and unexpired.
|
||||
// A missing account ID falls through to the locked path, which parses it from
|
||||
// the JWT.
|
||||
func (c *Client) currentToken() (access string, account string, ok bool) {
|
||||
c.tokenMu.Lock()
|
||||
defer c.tokenMu.Unlock()
|
||||
|
||||
access = strings.TrimSpace(c.AccessToken.Value)
|
||||
account = strings.TrimSpace(c.AccountID.Value)
|
||||
if access == "" || account == "" || tokenNeedsRefresh(access, time.Now()) {
|
||||
return "", "", false
|
||||
}
|
||||
return access, account, true
|
||||
}
|
||||
|
||||
func (c *Client) ensureAccessTokenLocked(ctx context.Context, forceRefresh bool) (string, string, error) {
|
||||
c.tokenMu.Lock()
|
||||
defer c.tokenMu.Unlock()
|
||||
|
||||
|
|
@ -61,7 +100,13 @@ func (c *Client) ensureAccessToken(ctx context.Context, forceRefresh bool) (stri
|
|||
c.setSettingValue(c.RefreshToken, refreshed.RefreshToken)
|
||||
}
|
||||
c.setSettingValue(c.AccountID, refreshedAccountID)
|
||||
debuglog.Debug(debuglog.Detailed, "Codex access token refreshed for account=%s\n", refreshedAccountID)
|
||||
if c.TokenPersist != nil {
|
||||
if err := c.TokenPersist(); err != nil {
|
||||
debuglog.Log("Codex token persist failed: %v\n", err)
|
||||
return "", "", fmt.Errorf(i18n.T("codex_token_persist_failed"), err)
|
||||
}
|
||||
}
|
||||
debuglog.Debug(debuglog.Detailed, "Codex access token refreshed account_present=%t\n", refreshedAccountID != "")
|
||||
|
||||
return c.AccessToken.Value, c.AccountID.Value, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ type Client struct {
|
|||
apiHTTPClient *http.Client
|
||||
|
||||
tokenMu sync.Mutex
|
||||
// TokenPersist writes current tokens after refresh. Nil skips. Error fails the refresh.
|
||||
TokenPersist func() error
|
||||
// WithStoreLock, when set, wraps refresh+persist (reload tokens inside the lock).
|
||||
WithStoreLock func(func() error) error
|
||||
}
|
||||
|
||||
type modelInfo struct {
|
||||
|
|
@ -81,11 +85,11 @@ func NewClient() *Client {
|
|||
client.AccountID = client.AddSetting("Account ID", true)
|
||||
|
||||
client.ApiBaseURL = client.AddSetupQuestionWithEnvName("Base URL", false,
|
||||
"Enter your Codex API base URL")
|
||||
i18n.T("codex_api_base_url_question"))
|
||||
client.ApiBaseURL.Value = defaultBaseURL
|
||||
|
||||
client.AuthBaseURL = client.AddSetupQuestionWithEnvName("Auth Base URL", false,
|
||||
"Enter your Codex OAuth base URL")
|
||||
i18n.T("codex_auth_base_url_question"))
|
||||
client.AuthBaseURL.Value = defaultAuthBaseURL
|
||||
|
||||
client.authHTTPClient = &http.Client{Timeout: modelsRequestTimeout}
|
||||
|
|
@ -101,6 +105,14 @@ func (c *Client) Setup() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(c.RefreshToken.Value) != "" {
|
||||
if err := c.configure(); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
debuglog.Log("Codex configure failed; starting browser login: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), oauthTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
|
@ -153,11 +165,26 @@ func (c *Client) configure() error {
|
|||
option.WithHTTPClient(c.apiHTTPClient),
|
||||
)
|
||||
c.ApiClient = &apiClient
|
||||
debuglog.Debug(debuglog.Detailed, "Codex configure: authenticated account=%s base_url=%s\n", c.AccountID.Value, c.ApiBaseURL.Value)
|
||||
debuglog.Debug(debuglog.Detailed, "Codex configure: authenticated account_present=%t base_url=%s\n", strings.TrimSpace(c.AccountID.Value) != "", c.ApiBaseURL.Value)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadEnvSettings copies non-empty Codex token values from env.
|
||||
func (c *Client) LoadEnvSettings(env map[string]string) {
|
||||
apply := func(setting *plugins.Setting) {
|
||||
if setting == nil || setting.EnvVariable == "" {
|
||||
return
|
||||
}
|
||||
if value := strings.TrimSpace(env[setting.EnvVariable]); value != "" {
|
||||
c.setSettingValue(setting, value)
|
||||
}
|
||||
}
|
||||
apply(c.AccessToken)
|
||||
apply(c.RefreshToken)
|
||||
apply(c.AccountID)
|
||||
}
|
||||
|
||||
// ListModels returns the Codex models available to the configured account.
|
||||
func (c *Client) ListModels(ctx context.Context) ([]string, error) {
|
||||
if c.apiHTTPClient == nil {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import (
|
|||
|
||||
"github.com/danielmiessler/fabric/internal/chat"
|
||||
"github.com/danielmiessler/fabric/internal/domain"
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
openaiapi "github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/shared/constant"
|
||||
)
|
||||
|
|
@ -211,6 +212,10 @@ func TestNormalizeSemverLikeVersion(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMapRequestErrorPreservesCodexAPIErrorMessage(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
|
||||
client := NewClient()
|
||||
apiErr := &openaiapi.Error{StatusCode: http.StatusBadRequest}
|
||||
if err := apiErr.UnmarshalJSON([]byte(`{"message":"The requested model is not supported.","type":"invalid_request_error","param":"model","code":"invalid_value"}`)); err != nil {
|
||||
|
|
@ -221,8 +226,9 @@ func TestMapRequestErrorPreservesCodexAPIErrorMessage(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Fatal("mapRequestError() returned nil")
|
||||
}
|
||||
if got := err.Error(); got != "codex request failed with status 400" {
|
||||
t.Fatalf("mapRequestError() = %q, want %q", got, "codex request failed with status 400")
|
||||
want := fmt.Sprintf(i18n.T("codex_request_failed_status"), http.StatusBadRequest)
|
||||
if got := err.Error(); got != want {
|
||||
t.Fatalf("mapRequestError() = %q, want %q", got, want)
|
||||
}
|
||||
if unwrapped := errors.Unwrap(err); unwrapped == nil || !strings.Contains(unwrapped.Error(), "The requested model is not supported.") {
|
||||
t.Fatalf("wrapped error = %v, want provider detail", unwrapped)
|
||||
|
|
@ -230,6 +236,10 @@ func TestMapRequestErrorPreservesCodexAPIErrorMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMapRequestErrorReadsAPIErrorResponseBodyWhenRawJSONMissing(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
|
||||
client := NewClient()
|
||||
apiErr := &openaiapi.Error{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
|
|
@ -242,8 +252,9 @@ func TestMapRequestErrorReadsAPIErrorResponseBodyWhenRawJSONMissing(t *testing.T
|
|||
if err == nil {
|
||||
t.Fatal("mapRequestError() returned nil")
|
||||
}
|
||||
if got := err.Error(); got != "codex request failed with status 400" {
|
||||
t.Fatalf("mapRequestError() = %q, want %q", got, "codex request failed with status 400")
|
||||
want := fmt.Sprintf(i18n.T("codex_request_failed_status"), http.StatusBadRequest)
|
||||
if got := err.Error(); got != want {
|
||||
t.Fatalf("mapRequestError() = %q, want %q", got, want)
|
||||
}
|
||||
if unwrapped := errors.Unwrap(err); unwrapped == nil || !strings.Contains(unwrapped.Error(), "The requested model is not supported for Codex.") {
|
||||
t.Fatalf("wrapped error = %v, want provider detail", unwrapped)
|
||||
|
|
@ -575,8 +586,8 @@ func TestSendStreamClosesChannelAndMapsHTTPError(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Fatal("SendStream() error = nil, want mapped HTTP error")
|
||||
}
|
||||
if got := err.Error(); got != "codex usage limit reached" {
|
||||
t.Fatalf("SendStream() error = %q, want %q", got, "codex usage limit reached")
|
||||
if got := err.Error(); got != i18n.T("codex_usage_limit_reached") {
|
||||
t.Fatalf("SendStream() error = %q, want %q", got, i18n.T("codex_usage_limit_reached"))
|
||||
}
|
||||
|
||||
update, ok := <-updates
|
||||
|
|
@ -585,6 +596,159 @@ func TestSendStreamClosesChannelAndMapsHTTPError(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEnsureAccessTokenPersistsRotatedRefreshToken(t *testing.T) {
|
||||
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/oauth/token" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(refreshResponse{
|
||||
IDToken: testJWT("acct_persist", time.Now().Add(2*time.Hour)),
|
||||
AccessToken: testJWT("acct_persist", time.Now().Add(2*time.Hour)),
|
||||
RefreshToken: "refresh-rotated",
|
||||
})
|
||||
}))
|
||||
defer authServer.Close()
|
||||
|
||||
var persisted map[string]string
|
||||
client := NewClient()
|
||||
client.ApiBaseURL.Value = "https://example.invalid"
|
||||
client.AuthBaseURL.Value = authServer.URL
|
||||
client.RefreshToken.Value = "refresh-old"
|
||||
client.AccessToken.Value = testJWT("acct_persist", time.Now().Add(-time.Hour))
|
||||
client.AccountID.Value = "acct_persist"
|
||||
client.TokenPersist = func() error {
|
||||
persisted = map[string]string{
|
||||
"access": client.AccessToken.Value,
|
||||
"refresh": client.RefreshToken.Value,
|
||||
"account": client.AccountID.Value,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
access, account, err := client.ensureAccessToken(context.Background(), false)
|
||||
if err != nil {
|
||||
t.Fatalf("ensureAccessToken() error = %v", err)
|
||||
}
|
||||
if access == "" {
|
||||
t.Fatal("ensureAccessToken() returned empty access token")
|
||||
}
|
||||
if account != "acct_persist" {
|
||||
t.Fatalf("account = %q, want acct_persist", account)
|
||||
}
|
||||
if persisted["refresh"] != "refresh-rotated" {
|
||||
t.Fatalf("persisted refresh = %q, want refresh-rotated", persisted["refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureAccessTokenPersistError(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
|
||||
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/oauth/token" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(refreshResponse{
|
||||
IDToken: testJWT("acct_persist", time.Now().Add(2*time.Hour)),
|
||||
AccessToken: testJWT("acct_persist", time.Now().Add(2*time.Hour)),
|
||||
RefreshToken: "refresh-rotated",
|
||||
})
|
||||
}))
|
||||
defer authServer.Close()
|
||||
|
||||
persistErr := errors.New("disk full")
|
||||
client := NewClient()
|
||||
client.ApiBaseURL.Value = "https://example.invalid"
|
||||
client.AuthBaseURL.Value = authServer.URL
|
||||
client.RefreshToken.Value = "refresh-old"
|
||||
client.AccessToken.Value = testJWT("acct_persist", time.Now().Add(-time.Hour))
|
||||
client.AccountID.Value = "acct_persist"
|
||||
client.TokenPersist = func() error {
|
||||
return persistErr
|
||||
}
|
||||
|
||||
_, _, err := client.ensureAccessToken(context.Background(), false)
|
||||
if err == nil {
|
||||
t.Fatal("ensureAccessToken() error = nil, want persist error")
|
||||
}
|
||||
if !errors.Is(err, persistErr) {
|
||||
t.Fatalf("ensureAccessToken() error = %v, want persist error %v", err, persistErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureAccessTokenReloadsStoreBeforeRefresh(t *testing.T) {
|
||||
authHits := 0
|
||||
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHits++
|
||||
http.Error(w, "unexpected refresh", http.StatusInternalServerError)
|
||||
}))
|
||||
defer authServer.Close()
|
||||
|
||||
fresh := testJWT("acct_reload", time.Now().Add(2*time.Hour))
|
||||
client := NewClient()
|
||||
client.ApiBaseURL.Value = "https://example.invalid"
|
||||
client.AuthBaseURL.Value = authServer.URL
|
||||
client.RefreshToken.Value = "refresh-old"
|
||||
client.AccessToken.Value = testJWT("acct_reload", time.Now().Add(-time.Hour))
|
||||
client.AccountID.Value = "acct_reload"
|
||||
client.WithStoreLock = func(fn func() error) error {
|
||||
client.AccessToken.Value = fresh
|
||||
client.AccountID.Value = "acct_reload"
|
||||
return fn()
|
||||
}
|
||||
|
||||
access, account, err := client.ensureAccessToken(context.Background(), false)
|
||||
if err != nil {
|
||||
t.Fatalf("ensureAccessToken() error = %v", err)
|
||||
}
|
||||
if access != fresh {
|
||||
t.Fatal("ensureAccessToken() did not use reloaded access token")
|
||||
}
|
||||
if account != "acct_reload" {
|
||||
t.Fatalf("account = %q, want acct_reload", account)
|
||||
}
|
||||
if authHits != 0 {
|
||||
t.Fatalf("auth server hits = %d, want 0", authHits)
|
||||
}
|
||||
}
|
||||
|
||||
// Regression: a valid in-memory token (e.g. just obtained via Setup's OAuth)
|
||||
// must not be reloaded from the store, which still holds the stale token until
|
||||
// SaveEnvFile runs. The fast path returns before WithStoreLock can clobber it.
|
||||
func TestEnsureAccessTokenKeepsFreshTokenOverStore(t *testing.T) {
|
||||
fresh := testJWT("acct_fresh", time.Now().Add(2*time.Hour))
|
||||
client := NewClient()
|
||||
client.ApiBaseURL.Value = "https://example.invalid"
|
||||
client.AuthBaseURL.Value = "https://auth.invalid"
|
||||
client.RefreshToken.Value = "refresh-new"
|
||||
client.AccessToken.Value = fresh
|
||||
client.AccountID.Value = "acct_fresh"
|
||||
|
||||
storeLocked := false
|
||||
client.WithStoreLock = func(fn func() error) error {
|
||||
storeLocked = true
|
||||
client.AccessToken.Value = "stale-from-disk"
|
||||
client.AccountID.Value = "acct_stale"
|
||||
return fn()
|
||||
}
|
||||
|
||||
access, account, err := client.ensureAccessToken(context.Background(), false)
|
||||
if err != nil {
|
||||
t.Fatalf("ensureAccessToken() error = %v", err)
|
||||
}
|
||||
if storeLocked {
|
||||
t.Fatal("store lock ran and clobbered a valid in-memory token")
|
||||
}
|
||||
if access != fresh || account != "acct_fresh" {
|
||||
t.Fatalf("got access=%q account=%q, want fresh token for acct_fresh", access, account)
|
||||
}
|
||||
}
|
||||
|
||||
func newConfiguredTestClient(t *testing.T, apiBaseURL string, accountID string, accessToken string) *Client {
|
||||
t.Helper()
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ func (c *Client) mapRequestError(err error) error {
|
|||
case isUsageLimitMessage(message):
|
||||
return &publicError{
|
||||
message: i18n.T("codex_usage_limit_reached"),
|
||||
cause: fmt.Errorf("codex request failed: %w", err),
|
||||
cause: fmt.Errorf(i18n.T("codex_request_failed"), err),
|
||||
}
|
||||
default:
|
||||
return err
|
||||
|
|
@ -91,7 +91,7 @@ func wrapPublicError(message string, statusCode int, providerMessage string) err
|
|||
|
||||
return &publicError{
|
||||
message: message,
|
||||
cause: fmt.Errorf("codex provider error (status %d): %s", statusCode, providerMessage),
|
||||
cause: fmt.Errorf(i18n.T("codex_provider_error"), statusCode, providerMessage),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ func (o *Client) ListModels(_ context.Context) (ret []string, err error) {
|
|||
|
||||
func (o *Client) SendStream(_ context.Context, msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions, channel chan domain.StreamUpdate) (err error) {
|
||||
ctx := context.Background()
|
||||
defer close(channel)
|
||||
|
||||
var req ollamaapi.ChatRequest
|
||||
if req, err = o.createChatRequest(ctx, msgs, opts); err != nil {
|
||||
|
|
@ -135,7 +136,6 @@ func (o *Client) SendStream(_ context.Context, msgs []*chat.ChatCompletionMessag
|
|||
return
|
||||
}
|
||||
|
||||
close(channel)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/chat"
|
||||
"github.com/danielmiessler/fabric/internal/domain"
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
ollamaapi "github.com/ollama/ollama/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -60,3 +64,28 @@ func TestLoadImageBytes_DataURLSuccess(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
|
||||
func TestSendStreamClosesChannelOnChatError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(`{"error":"ollama failed"}` + "\n"))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
baseURL, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := &Client{client: ollamaapi.NewClient(baseURL, server.Client())}
|
||||
channel := make(chan domain.StreamUpdate)
|
||||
|
||||
err = client.SendStream(
|
||||
context.Background(),
|
||||
[]*chat.ChatCompletionMessage{{Role: chat.ChatMessageRoleUser, Content: "hello"}},
|
||||
&domain.ChatOptions{Model: "missing-model"},
|
||||
channel,
|
||||
)
|
||||
|
||||
require.Error(t, err)
|
||||
_, ok := <-channel
|
||||
assert.False(t, ok, "stream channel should be closed when Ollama chat returns an error")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ func (o *Client) NeedsRawMode(modelName string) bool {
|
|||
openaiModelsPrefixes := []string{
|
||||
"glm",
|
||||
"gpt-5",
|
||||
"gpt-6",
|
||||
"o1",
|
||||
"o3",
|
||||
"o4",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNeedsRawModeGPT6(t *testing.T) {
|
||||
if !NewClient().NeedsRawMode("gpt-6-astra") {
|
||||
t.Fatal("gpt-6 models should use raw mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResponseRequestWithMaxTokens(t *testing.T) {
|
||||
|
||||
var msgs []*chat.ChatCompletionMessage
|
||||
|
|
|
|||
|
|
@ -289,11 +289,21 @@ var ProviderMap = map[string]ProviderConfig{
|
|||
BaseURL: "https://openrouter.ai/api/v1",
|
||||
ImplementsResponses: false,
|
||||
},
|
||||
"Pzero": {
|
||||
Name: "Pzero",
|
||||
BaseURL: "https://api.pzero.studio/v1",
|
||||
ImplementsResponses: false,
|
||||
},
|
||||
"SiliconCloud": {
|
||||
Name: "SiliconCloud",
|
||||
BaseURL: "https://api.siliconflow.cn/v1",
|
||||
ImplementsResponses: false,
|
||||
},
|
||||
"Synthorai": {
|
||||
Name: "Synthorai",
|
||||
BaseURL: "https://synthorai.io/v1",
|
||||
ImplementsResponses: false,
|
||||
},
|
||||
"Together": {
|
||||
Name: "Together",
|
||||
BaseURL: "https://api.together.xyz/v1",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
package db
|
||||
|
||||
// Storage is the contract for a named-entity store. Each implementation
|
||||
// specifies the names that are valid. It rejects an invalid name with a
|
||||
// typed error. Callers can map this error to a client error.
|
||||
type Storage[T any] interface {
|
||||
Configure() (err error)
|
||||
Get(name string) (ret *T, err error)
|
||||
GetNames() (ret []string, err error)
|
||||
Delete(name string) (err error)
|
||||
// Exists reports false for an invalid name. It cannot show the
|
||||
// difference between a rejected name and an absent entry.
|
||||
Exists(name string) (ret bool)
|
||||
Rename(oldName, newName string) (err error)
|
||||
Save(name string, content []byte) (err error)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
|
|
@ -41,6 +42,8 @@ type Db struct {
|
|||
Contexts *ContextsEntity
|
||||
|
||||
EnvFilePath string
|
||||
|
||||
envMu sync.Mutex
|
||||
}
|
||||
|
||||
func (o *Db) Configure() (err error) {
|
||||
|
|
@ -92,9 +95,100 @@ func (o *Db) IsEnvFileExists() (ret bool) {
|
|||
return
|
||||
}
|
||||
|
||||
func (o *Db) SaveEnv(content string) (err error) {
|
||||
err = os.WriteFile(o.EnvFilePath, []byte(content), 0644)
|
||||
return
|
||||
func (o *Db) SaveEnv(content string) error {
|
||||
return o.WithEnvLock(func() error {
|
||||
if err := writeFileAtomic(o.EnvFilePath, []byte(content)); err != nil {
|
||||
return fmt.Errorf(i18n.T("db_error_updating_env_file"), err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (o *Db) ReadEnvFile() (map[string]string, error) {
|
||||
env, err := godotenv.Read(o.EnvFilePath)
|
||||
if err != nil {
|
||||
if o.IsEnvFileExists() {
|
||||
return nil, fmt.Errorf(i18n.T("db_error_loading_env_file"), err)
|
||||
}
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func (o *Db) WithEnvLock(fn func() error) error {
|
||||
o.envMu.Lock()
|
||||
defer o.envMu.Unlock()
|
||||
|
||||
lockFile, err := os.OpenFile(o.EnvFilePath+".lock", os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf(i18n.T("db_error_updating_env_file"), err)
|
||||
}
|
||||
defer lockFile.Close()
|
||||
if err := lockExclusive(lockFile); err != nil {
|
||||
return fmt.Errorf(i18n.T("db_error_updating_env_file"), err)
|
||||
}
|
||||
defer unlockExclusive(lockFile)
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
||||
// UpdateEnvVars merges non-empty values into .env under a file lock.
|
||||
// Comments and key order are not preserved.
|
||||
func (o *Db) UpdateEnvVars(updates map[string]string) error {
|
||||
return o.WithEnvLock(func() error {
|
||||
return o.ApplyEnvUpdates(updates)
|
||||
})
|
||||
}
|
||||
|
||||
// ApplyEnvUpdates writes non-empty updates atomically. Callers holding WithEnvLock use this.
|
||||
func (o *Db) ApplyEnvUpdates(updates map[string]string) error {
|
||||
env, err := o.ReadEnvFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range updates {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
continue
|
||||
}
|
||||
env[key] = value
|
||||
}
|
||||
if err := writeEnvFileAtomic(o.EnvFilePath, env); err != nil {
|
||||
return fmt.Errorf(i18n.T("db_error_updating_env_file"), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeEnvFileAtomic(path string, env map[string]string) error {
|
||||
content, err := godotenv.Marshal(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeFileAtomic(path, []byte(content+"\n"))
|
||||
}
|
||||
|
||||
func writeFileAtomic(path string, content []byte) error {
|
||||
tmp, err := os.CreateTemp(filepath.Dir(path), ".env.tmp-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
defer os.Remove(tmpName)
|
||||
|
||||
if err := tmp.Chmod(0600); err != nil {
|
||||
_ = tmp.Close()
|
||||
return err
|
||||
}
|
||||
_, err = tmp.Write(content)
|
||||
if err == nil {
|
||||
err = tmp.Sync()
|
||||
}
|
||||
if cerr := tmp.Close(); err == nil {
|
||||
err = cerr
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpName, path)
|
||||
}
|
||||
|
||||
func (o *Db) FilePath(fileName string) (ret string) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@ package fsdb
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func TestDb_Configure(t *testing.T) {
|
||||
|
|
@ -52,4 +57,150 @@ func TestDb_SaveEnv(t *testing.T) {
|
|||
if _, err := os.Stat(db.EnvFilePath); os.IsNotExist(err) {
|
||||
t.Errorf("expected .env file to be saved")
|
||||
}
|
||||
assertEnvMode(t, db.EnvFilePath)
|
||||
}
|
||||
|
||||
func TestDb_UpdateEnvVars(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db := NewDb(dir)
|
||||
if err := db.SaveEnv("KEEP=old\nCODEX_REFRESH_TOKEN=stale\n"); err != nil {
|
||||
t.Fatalf("SaveEnv() error = %v", err)
|
||||
}
|
||||
|
||||
if err := db.UpdateEnvVars(map[string]string{
|
||||
"CODEX_REFRESH_TOKEN": "rotated",
|
||||
"CODEX_ACCESS_TOKEN": "fresh",
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateEnvVars() error = %v", err)
|
||||
}
|
||||
|
||||
parsed, err := godotenv.Read(db.EnvFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("godotenv.Read() error = %v", err)
|
||||
}
|
||||
if parsed["KEEP"] != "old" {
|
||||
t.Fatalf("KEEP = %q, want old", parsed["KEEP"])
|
||||
}
|
||||
if parsed["CODEX_REFRESH_TOKEN"] != "rotated" {
|
||||
t.Fatalf("CODEX_REFRESH_TOKEN = %q, want rotated", parsed["CODEX_REFRESH_TOKEN"])
|
||||
}
|
||||
if parsed["CODEX_ACCESS_TOKEN"] != "fresh" {
|
||||
t.Fatalf("CODEX_ACCESS_TOKEN = %q, want fresh", parsed["CODEX_ACCESS_TOKEN"])
|
||||
}
|
||||
assertEnvMode(t, db.EnvFilePath)
|
||||
}
|
||||
|
||||
func TestDb_UpdateEnvVars_MissingFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db := NewDb(dir)
|
||||
|
||||
if err := db.UpdateEnvVars(map[string]string{"CODEX_ACCESS_TOKEN": "fresh"}); err != nil {
|
||||
t.Fatalf("UpdateEnvVars() error = %v", err)
|
||||
}
|
||||
|
||||
parsed, err := godotenv.Read(db.EnvFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("godotenv.Read() error = %v", err)
|
||||
}
|
||||
if parsed["CODEX_ACCESS_TOKEN"] != "fresh" {
|
||||
t.Fatalf("CODEX_ACCESS_TOKEN = %q, want fresh", parsed["CODEX_ACCESS_TOKEN"])
|
||||
}
|
||||
assertEnvMode(t, db.EnvFilePath)
|
||||
}
|
||||
|
||||
func TestDb_UpdateEnvVars_CorruptFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db := NewDb(dir)
|
||||
if err := os.Mkdir(db.EnvFilePath, 0700); err != nil {
|
||||
t.Fatalf("Mkdir() error = %v", err)
|
||||
}
|
||||
|
||||
err := db.UpdateEnvVars(map[string]string{"CODEX_ACCESS_TOKEN": "fresh"})
|
||||
if err == nil {
|
||||
t.Fatal("UpdateEnvVars() error = nil, want corrupt-file error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDb_UpdateEnvVars_SkipEmpty(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db := NewDb(dir)
|
||||
if err := db.SaveEnv("CODEX_REFRESH_TOKEN=live\nKEEP=old\n"); err != nil {
|
||||
t.Fatalf("SaveEnv() error = %v", err)
|
||||
}
|
||||
|
||||
if err := db.UpdateEnvVars(map[string]string{
|
||||
"CODEX_REFRESH_TOKEN": "",
|
||||
"CODEX_ACCESS_TOKEN": "fresh",
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateEnvVars() error = %v", err)
|
||||
}
|
||||
|
||||
parsed, err := godotenv.Read(db.EnvFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("godotenv.Read() error = %v", err)
|
||||
}
|
||||
if parsed["CODEX_REFRESH_TOKEN"] != "live" {
|
||||
t.Fatalf("CODEX_REFRESH_TOKEN = %q, want live", parsed["CODEX_REFRESH_TOKEN"])
|
||||
}
|
||||
if parsed["CODEX_ACCESS_TOKEN"] != "fresh" {
|
||||
t.Fatalf("CODEX_ACCESS_TOKEN = %q, want fresh", parsed["CODEX_ACCESS_TOKEN"])
|
||||
}
|
||||
if parsed["KEEP"] != "old" {
|
||||
t.Fatalf("KEEP = %q, want old", parsed["KEEP"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDb_UpdateEnvVars_Concurrent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
db := NewDb(dir)
|
||||
if err := db.SaveEnv("KEEP=old\n"); err != nil {
|
||||
t.Fatalf("SaveEnv() error = %v", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, 2)
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errs <- db.UpdateEnvVars(map[string]string{"CODEX_ACCESS_TOKEN": "one"})
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errs <- db.UpdateEnvVars(map[string]string{"CODEX_REFRESH_TOKEN": "two"})
|
||||
}()
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateEnvVars() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
parsed, err := godotenv.Read(db.EnvFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("godotenv.Read() error = %v", err)
|
||||
}
|
||||
if parsed["KEEP"] != "old" {
|
||||
t.Fatalf("KEEP = %q, want old", parsed["KEEP"])
|
||||
}
|
||||
if parsed["CODEX_ACCESS_TOKEN"] != "one" {
|
||||
t.Fatalf("CODEX_ACCESS_TOKEN = %q, want one", parsed["CODEX_ACCESS_TOKEN"])
|
||||
}
|
||||
if parsed["CODEX_REFRESH_TOKEN"] != "two" {
|
||||
t.Fatalf("CODEX_REFRESH_TOKEN = %q, want two", parsed["CODEX_REFRESH_TOKEN"])
|
||||
}
|
||||
}
|
||||
|
||||
func assertEnvMode(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
return
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(%s) error = %v", path, err)
|
||||
}
|
||||
if perm := info.Mode().Perm(); perm != 0600 {
|
||||
t.Fatalf("%s mode = %o, want 0600", filepath.Base(path), perm)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
internal/plugins/db/fsdb/env_lock_unix.go
Normal file
22
internal/plugins/db/fsdb/env_lock_unix.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//go:build unix
|
||||
|
||||
package fsdb
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func lockExclusive(f *os.File) error {
|
||||
err := unix.Flock(int(f.Fd()), unix.LOCK_EX)
|
||||
runtime.KeepAlive(f)
|
||||
return err
|
||||
}
|
||||
|
||||
func unlockExclusive(f *os.File) error {
|
||||
err := unix.Flock(int(f.Fd()), unix.LOCK_UN)
|
||||
runtime.KeepAlive(f)
|
||||
return err
|
||||
}
|
||||
19
internal/plugins/db/fsdb/env_lock_windows.go
Normal file
19
internal/plugins/db/fsdb/env_lock_windows.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//go:build windows
|
||||
|
||||
package fsdb
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func lockExclusive(f *os.File) error {
|
||||
var overlapped windows.Overlapped
|
||||
return windows.LockFileEx(windows.Handle(f.Fd()), windows.LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &overlapped)
|
||||
}
|
||||
|
||||
func unlockExclusive(f *os.File) error {
|
||||
var overlapped windows.Overlapped
|
||||
return windows.UnlockFileEx(windows.Handle(f.Fd()), 0, 1, 0, &overlapped)
|
||||
}
|
||||
|
|
@ -55,14 +55,18 @@ func (o *PatternsEntity) GetRaw(name string) (*Pattern, error) {
|
|||
return o.getFromDB(name)
|
||||
}
|
||||
|
||||
func (o *PatternsEntity) loadPattern(source string) (pattern *Pattern, err error) {
|
||||
// Determine if this is a file path
|
||||
isFilePath := strings.HasPrefix(source, "\\") ||
|
||||
// LooksLikePatternFilePath reports whether loadPattern uses source as a
|
||||
// filesystem path. HTTP handlers must reject these names to keep the
|
||||
// CLI file-path feature out of the REST API.
|
||||
func LooksLikePatternFilePath(source string) bool {
|
||||
return strings.HasPrefix(source, "\\") ||
|
||||
strings.HasPrefix(source, "/") ||
|
||||
strings.HasPrefix(source, "~") ||
|
||||
strings.HasPrefix(source, ".")
|
||||
}
|
||||
|
||||
if isFilePath {
|
||||
func (o *PatternsEntity) loadPattern(source string) (pattern *Pattern, err error) {
|
||||
if LooksLikePatternFilePath(source) {
|
||||
// Resolve the file path using GetAbsolutePath
|
||||
var absPath string
|
||||
if absPath, err = util.GetAbsolutePath(source); err != nil {
|
||||
|
|
@ -119,8 +123,13 @@ func (o *PatternsEntity) applyVariables(
|
|||
|
||||
// retrieves a pattern from the database by name
|
||||
func (o *PatternsEntity) getFromDB(name string) (ret *Pattern, err error) {
|
||||
if strings.Contains(name, "..") {
|
||||
return nil, fmt.Errorf(i18n.T("pattern_invalid_name"), name)
|
||||
if ValidateStorageName(name) != nil {
|
||||
// The typed error lets an HTTP route without a pre-validation
|
||||
// guard map this rejection to 400, not 500.
|
||||
return nil, &InvalidStorageNameError{
|
||||
Name: name,
|
||||
Message: fmt.Sprintf(i18n.T("pattern_invalid_name"), name),
|
||||
}
|
||||
}
|
||||
|
||||
// First check custom patterns directory if it exists
|
||||
|
|
@ -283,13 +292,45 @@ func (o *PatternsEntity) Get(name string) (*Pattern, error) {
|
|||
return o.GetApplyVariables(name, nil, "")
|
||||
}
|
||||
func (o *PatternsEntity) Save(name string, content []byte) (err error) {
|
||||
patternDir := filepath.Join(o.Dir, name)
|
||||
// Do not store a name that loadPattern uses as a file path, for
|
||||
// example ".foo" or "~bar". For such a name, GetApplyVariables reads
|
||||
// from the filesystem, not from the database.
|
||||
if LooksLikePatternFilePath(name) {
|
||||
return &InvalidStorageNameError{
|
||||
Name: name,
|
||||
Message: fmt.Sprintf(i18n.T("pattern_invalid_name"), name),
|
||||
}
|
||||
}
|
||||
var patternDir string
|
||||
if patternDir, err = o.resolvedPath(name); err != nil {
|
||||
return
|
||||
}
|
||||
if err = os.MkdirAll(patternDir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf(i18n.T("patterns_error_create_directory"), err)
|
||||
}
|
||||
patternPath := filepath.Join(patternDir, o.SystemPatternFile)
|
||||
// The pattern file can be a symlink that already exists. Do not
|
||||
// write through a symlink that goes out of the pattern directory.
|
||||
if err = symlinkContained(patternDir, patternPath, name); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.WriteFile(patternPath, content, 0644); err != nil {
|
||||
return fmt.Errorf(i18n.T("patterns_error_save_pattern"), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rename applies the file-path guard from Save to the destination name.
|
||||
// Without the guard, the inherited StorageEntity.Rename accepts ".foo"
|
||||
// or "~foo", and loadPattern then reads these names from the
|
||||
// filesystem. A path-like source stays permitted, which lets you rename
|
||||
// a legacy entry to a valid name.
|
||||
func (o *PatternsEntity) Rename(oldName, newName string) error {
|
||||
if LooksLikePatternFilePath(newName) {
|
||||
return &InvalidStorageNameError{
|
||||
Name: newName,
|
||||
Message: fmt.Sprintf(i18n.T("pattern_invalid_name"), newName),
|
||||
}
|
||||
}
|
||||
return o.StorageEntity.Rename(oldName, newName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package fsdb
|
|||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -179,6 +181,88 @@ func TestPatternsEntity_Save(t *testing.T) {
|
|||
assert.Equal(t, content, data)
|
||||
}
|
||||
|
||||
// Save must reject a name that loadPattern uses as a filesystem path,
|
||||
// and that includes a traversal name. For such a name,
|
||||
// GetApplyVariables reads from the disk, not from the database.
|
||||
func TestPatternsEntity_SaveRejectsFilePathNames(t *testing.T) {
|
||||
entity, cleanup := setupTestPatternsEntity(t)
|
||||
defer cleanup()
|
||||
|
||||
for _, name := range []string{"..", ".foo", "..bar", "~bar", "/abs/path", `\win\path`} {
|
||||
err := entity.Save(name, []byte("pwned"))
|
||||
assert.Error(t, err, "expected error for file-path name: %q", name)
|
||||
if strings.HasPrefix(name, "/") || strings.HasPrefix(name, `\`) {
|
||||
// If Save accepts an absolute name, it does not write in
|
||||
// entity.Dir, and the join below points to the incorrect
|
||||
// location. For these two names, only the error assertion
|
||||
// gives protection.
|
||||
continue
|
||||
}
|
||||
// For ".." this path is the system.md of the parent directory,
|
||||
// which Save makes if it accepts a traversal name.
|
||||
_, statErr := os.Stat(filepath.Join(entity.Dir, name, entity.SystemPatternFile))
|
||||
assert.True(t, os.IsNotExist(statErr), "wrote a pattern file for: %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Rename must reject a file-path-like destination, the same as Save.
|
||||
// The inherited StorageEntity.Rename accepts such a destination.
|
||||
func TestPatternsEntity_RenameRejectsFilePathDestination(t *testing.T) {
|
||||
entity, cleanup := setupTestPatternsEntity(t)
|
||||
defer cleanup()
|
||||
|
||||
createTestPattern(t, entity, "good-name", "content")
|
||||
for _, newName := range []string{".foo", "~bar"} {
|
||||
err := entity.Rename("good-name", newName)
|
||||
var invalidName *InvalidStorageNameError
|
||||
require.ErrorAs(t, err, &invalidName, "expected rejection for destination: %q", newName)
|
||||
_, statErr := os.Stat(filepath.Join(entity.Dir, newName))
|
||||
assert.True(t, os.IsNotExist(statErr), "renamed to: %q", newName)
|
||||
}
|
||||
|
||||
// A valid destination still works.
|
||||
require.NoError(t, entity.Rename("good-name", "better-name"))
|
||||
_, err := os.Stat(filepath.Join(entity.Dir, "better-name"))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Save must not write through a symlinked pattern directory or a
|
||||
// symlinked pattern file that points outside the storage tree.
|
||||
func TestPatternsEntity_SaveRejectsSymlinkEscape(t *testing.T) {
|
||||
entity, cleanup := setupTestPatternsEntity(t)
|
||||
defer cleanup()
|
||||
|
||||
outsideDir := t.TempDir()
|
||||
mustSymlink(t, outsideDir, filepath.Join(entity.Dir, "linked-dir"))
|
||||
err := entity.Save("linked-dir", []byte("pwned"))
|
||||
var invalidName *InvalidStorageNameError
|
||||
require.ErrorAs(t, err, &invalidName)
|
||||
_, statErr := os.Stat(filepath.Join(outsideDir, entity.SystemPatternFile))
|
||||
assert.True(t, os.IsNotExist(statErr), "wrote through the symlinked pattern dir")
|
||||
|
||||
outsideFile := filepath.Join(outsideDir, "target.md")
|
||||
require.NoError(t, os.WriteFile(outsideFile, []byte("keep"), 0o644))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(entity.Dir, "real-pattern"), 0o755))
|
||||
mustSymlink(t, outsideFile, filepath.Join(entity.Dir, "real-pattern", entity.SystemPatternFile))
|
||||
err = entity.Save("real-pattern", []byte("pwned"))
|
||||
require.ErrorAs(t, err, &invalidName)
|
||||
got, readErr := os.ReadFile(outsideFile)
|
||||
require.NoError(t, readErr)
|
||||
assert.Equal(t, "keep", string(got), "outside pattern file was overwritten")
|
||||
}
|
||||
|
||||
func TestGetApplyVariables_FromFile(t *testing.T) {
|
||||
entity, cleanup := setupTestPatternsEntity(t)
|
||||
defer cleanup()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "fromfile.md")
|
||||
require.NoError(t, os.WriteFile(path, []byte("Hello {{input}}"), 0o644))
|
||||
|
||||
result, err := entity.GetApplyVariables(path, nil, "world")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Hello world", result.Pattern)
|
||||
}
|
||||
|
||||
func TestPatternsEntity_CustomPatterns(t *testing.T) {
|
||||
// Create main patterns directory
|
||||
mainDir, err := os.MkdirTemp("", "test-main-patterns-*")
|
||||
|
|
@ -333,20 +417,43 @@ func TestPrintPattern(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetFromDB_PathTraversal(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
|
||||
entity, cleanup := setupTestPatternsEntity(t)
|
||||
defer cleanup()
|
||||
|
||||
traversalNames := []string{
|
||||
"../etc/passwd",
|
||||
"../../secret",
|
||||
"foo/../bar",
|
||||
"..",
|
||||
"valid/../../../etc/shadow",
|
||||
for _, name := range invalidStorageNames {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := entity.GetRaw(name)
|
||||
require.Error(t, err, "expected error for traversal name: %q", name)
|
||||
assert.Contains(t, err.Error(), "invalid pattern name", "wrong error for: %q", name)
|
||||
var invalidName *InvalidStorageNameError
|
||||
assert.ErrorAs(t, err, &invalidName, "want typed rejection for: %q", name)
|
||||
})
|
||||
}
|
||||
for _, name := range traversalNames {
|
||||
_, err := entity.GetRaw(name)
|
||||
assert.Error(t, err, "expected error for traversal name: %q", name)
|
||||
assert.Contains(t, err.Error(), "invalid pattern name", "wrong error for: %q", name)
|
||||
}
|
||||
|
||||
// A ".." in a name without separators is one safe path element.
|
||||
// ValidateStorageName accepts it, and getFromDB also accepts it.
|
||||
func TestGetFromDB_AllowsDotsWithinName(t *testing.T) {
|
||||
entity, cleanup := setupTestPatternsEntity(t)
|
||||
defer cleanup()
|
||||
|
||||
createTestPattern(t, entity, "foo..bar", "dotty {{input}}")
|
||||
|
||||
pattern, err := entity.GetRaw("foo..bar")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "dotty {{input}}", pattern.Pattern)
|
||||
}
|
||||
|
||||
func TestLooksLikePatternFilePath(t *testing.T) {
|
||||
for _, source := range []string{"/x", `~\x`, `\x`, `.\x`, "~", ".", ".."} {
|
||||
assert.True(t, LooksLikePatternFilePath(source), "expected file-path detection for: %q", source)
|
||||
}
|
||||
for _, source := range []string{"", "pattern", "foo..bar", "a/b", "x~y", "x.y"} {
|
||||
assert.False(t, LooksLikePatternFilePath(source), "unexpected file-path detection for: %q", source)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ type SessionsEntity struct {
|
|||
}
|
||||
|
||||
func (o *SessionsEntity) Get(name string) (session *Session, err error) {
|
||||
// Reject invalid names here. Exists reports false for them, and the
|
||||
// missing-session branch then answers with a new empty session and
|
||||
// no error.
|
||||
if err = ValidateStorageName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session = &Session{Name: name}
|
||||
|
||||
if o.Exists(name) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,20 @@ func TestSessions_GetOrCreateSession(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Get must reject an invalid name and must not answer with a new empty
|
||||
// session. GET /sessions/<bad-name> is then a 400, the same as for the
|
||||
// other entities.
|
||||
func TestSessions_GetRejectsInvalidNames(t *testing.T) {
|
||||
sessions := &SessionsEntity{
|
||||
StorageEntity: &StorageEntity{Dir: t.TempDir(), FileExtension: ".json"},
|
||||
}
|
||||
for _, name := range invalidStorageNames {
|
||||
if _, err := sessions.Get(name); err == nil {
|
||||
t.Errorf("Get(%q) succeeded, want error", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessions_SaveSession(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
sessions := &SessionsEntity{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import (
|
|||
"github.com/danielmiessler/fabric/internal/util"
|
||||
)
|
||||
|
||||
// StorageEntity is the filesystem-backed db.Storage implementation.
|
||||
// Each method that gets a name requires one that obeys
|
||||
// ValidateStorageName. It rejects other names with
|
||||
// *InvalidStorageNameError, which HTTP handlers map to 400.
|
||||
type StorageEntity struct {
|
||||
Label string
|
||||
Dir string
|
||||
|
|
@ -68,34 +72,57 @@ func (o *StorageEntity) GetNames() (ret []string, err error) {
|
|||
}
|
||||
|
||||
func (o *StorageEntity) Delete(name string) (err error) {
|
||||
if err = os.RemoveAll(o.BuildFilePathByName(name)); err != nil {
|
||||
var path string
|
||||
if path, err = o.resolvedPath(name); err != nil {
|
||||
return
|
||||
}
|
||||
if err = os.RemoveAll(path); err != nil {
|
||||
err = fmt.Errorf(i18n.T("storage_error_delete"), name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *StorageEntity) Exists(name string) (ret bool) {
|
||||
_, err := os.Stat(o.BuildFilePathByName(name))
|
||||
path, err := o.resolvedPath(name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = os.Stat(path)
|
||||
ret = !os.IsNotExist(err)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *StorageEntity) Rename(oldName, newName string) (err error) {
|
||||
if err = os.Rename(o.BuildFilePathByName(oldName), o.BuildFilePathByName(newName)); err != nil {
|
||||
var oldPath, newPath string
|
||||
if oldPath, err = o.resolvedPath(oldName); err != nil {
|
||||
return
|
||||
}
|
||||
if newPath, err = o.resolvedPath(newName); err != nil {
|
||||
return
|
||||
}
|
||||
if err = os.Rename(oldPath, newPath); err != nil {
|
||||
err = fmt.Errorf(i18n.T("storage_error_rename"), oldName, newName, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *StorageEntity) Save(name string, content []byte) (err error) {
|
||||
if err = os.WriteFile(o.BuildFilePathByName(name), content, 0644); err != nil {
|
||||
var path string
|
||||
if path, err = o.resolvedPath(name); err != nil {
|
||||
return
|
||||
}
|
||||
if err = os.WriteFile(path, content, 0644); err != nil {
|
||||
err = fmt.Errorf(i18n.T("storage_error_save"), name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *StorageEntity) Load(name string) (ret []byte, err error) {
|
||||
if ret, err = os.ReadFile(o.BuildFilePathByName(name)); err != nil {
|
||||
var path string
|
||||
if path, err = o.resolvedPath(name); err != nil {
|
||||
return
|
||||
}
|
||||
if ret, err = os.ReadFile(path); err != nil {
|
||||
err = fmt.Errorf(i18n.T("storage_error_load"), name, err)
|
||||
}
|
||||
return
|
||||
|
|
@ -120,11 +147,6 @@ func (o *StorageEntity) ListNames(shellCompleteList bool) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (o *StorageEntity) BuildFilePathByName(name string) (ret string) {
|
||||
ret = o.BuildFilePath(o.buildFileName(name))
|
||||
return
|
||||
}
|
||||
|
||||
func (o *StorageEntity) BuildFilePath(fileName string) (ret string) {
|
||||
ret = filepath.Join(o.Dir, fileName)
|
||||
return
|
||||
|
|
@ -134,6 +156,122 @@ func (o *StorageEntity) buildFileName(name string) string {
|
|||
return fmt.Sprintf("%s%v", name, o.FileExtension)
|
||||
}
|
||||
|
||||
// InvalidStorageNameError reports a name that storage-name validation
|
||||
// rejected. HTTP handlers map it to 400 Bad Request. All other storage
|
||||
// errors stay 500 errors.
|
||||
type InvalidStorageNameError struct {
|
||||
Name string
|
||||
Message string // optional: the default is the storage_invalid_name translation
|
||||
}
|
||||
|
||||
func (e *InvalidStorageNameError) Error() string {
|
||||
if e.Message != "" {
|
||||
return e.Message
|
||||
}
|
||||
return fmt.Sprintf(i18n.T("storage_invalid_name"), e.Name)
|
||||
}
|
||||
|
||||
// windowsReservedNames are DOS device names. On Windows, these names
|
||||
// identify devices, not files. The match ignores case and all text after
|
||||
// the first dot, because Windows maps "CON.tar.gz" to the CON device.
|
||||
var windowsReservedNames = map[string]bool{
|
||||
"CON": true, "PRN": true, "AUX": true, "NUL": true,
|
||||
"COM1": true, "COM2": true, "COM3": true, "COM4": true, "COM5": true,
|
||||
"COM6": true, "COM7": true, "COM8": true, "COM9": true,
|
||||
"LPT1": true, "LPT2": true, "LPT3": true, "LPT4": true, "LPT5": true,
|
||||
"LPT6": true, "LPT7": true, "LPT8": true, "LPT9": true,
|
||||
}
|
||||
|
||||
// ValidateStorageName rejects an empty name, ".", "..", and each name
|
||||
// that is not a single path element. It also rejects names that are
|
||||
// dangerous only on Windows: names with ":" (an NTFS alternate data
|
||||
// stream suffix), names with a dot or space at the end, and reserved
|
||||
// DOS device names. Windows removes a dot or space at the end, and the
|
||||
// shortened name then collides with an existing entry. The policy is
|
||||
// the same on each platform, and an entry made on one system stays
|
||||
// valid on the other systems. A Unix
|
||||
// entry that already has a name against these rules shows in GetNames
|
||||
// but is not accessible. To repair it, rename its file or directory on
|
||||
// disk. Call this function before you join a name to a storage
|
||||
// directory.
|
||||
func ValidateStorageName(name string) error {
|
||||
if name == "" || name == "." || name == ".." {
|
||||
return &InvalidStorageNameError{Name: name}
|
||||
}
|
||||
if strings.ContainsAny(name, `/\:`) {
|
||||
return &InvalidStorageNameError{Name: name}
|
||||
}
|
||||
if name != strings.TrimRight(name, ". ") {
|
||||
return &InvalidStorageNameError{Name: name}
|
||||
}
|
||||
base := strings.ToUpper(name)
|
||||
if i := strings.IndexByte(base, '.'); i >= 0 {
|
||||
base = base[:i]
|
||||
}
|
||||
if windowsReservedNames[base] {
|
||||
return &InvalidStorageNameError{Name: name}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// symlinkContained rejects an entry at path if the entry resolves,
|
||||
// through symlinks, to a target outside absDir. A missing entry passes,
|
||||
// because the lexical check in resolvedPath already keeps the path that
|
||||
// a write will make in the directory. The two inputs must be absolute
|
||||
// paths. If they are not, you cannot compare the resolved forms. The
|
||||
// check does not fully prevent local races. If a hostile local writer
|
||||
// enters the threat model, move to os.Root.
|
||||
func symlinkContained(absDir, path, name string) error {
|
||||
target, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if _, lerr := os.Lstat(path); os.IsNotExist(lerr) {
|
||||
return nil
|
||||
}
|
||||
// This is a dangling symlink. A write through it makes the
|
||||
// outside target.
|
||||
return &InvalidStorageNameError{Name: name}
|
||||
}
|
||||
return err
|
||||
}
|
||||
resolvedDir, err := filepath.EvalSymlinks(absDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(resolvedDir, target)
|
||||
if err != nil || !filepath.IsLocal(rel) {
|
||||
return &InvalidStorageNameError{Name: name}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolvedPath keeps name in the entity directory. It validates the
|
||||
// name, checks containment again after absolute resolution, and
|
||||
// rejects a symlinked entry that resolves out of the directory.
|
||||
// Symlinks that stay in the directory are permitted. A storage
|
||||
// directory that is a symlink is also permitted.
|
||||
func (o *StorageEntity) resolvedPath(name string) (string, error) {
|
||||
if err := ValidateStorageName(name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
absDir, err := filepath.Abs(o.Dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
absFull, err := filepath.Abs(filepath.Join(o.Dir, o.buildFileName(name)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rel, err := filepath.Rel(absDir, absFull)
|
||||
if err != nil || !filepath.IsLocal(rel) {
|
||||
return "", &InvalidStorageNameError{Name: name}
|
||||
}
|
||||
if err := symlinkContained(absDir, absFull, name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return absFull, nil
|
||||
}
|
||||
|
||||
func (o *StorageEntity) SaveAsJson(name string, item any) (err error) {
|
||||
var jsonString []byte
|
||||
if jsonString, err = json.Marshal(item); err == nil {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package fsdb
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
)
|
||||
|
||||
func TestStorage_SaveAndLoad(t *testing.T) {
|
||||
|
|
@ -50,3 +54,197 @@ func TestStorage_Delete(t *testing.T) {
|
|||
t.Errorf("expected file to be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
// invalidStorageNames are names that ValidateStorageName must reject on
|
||||
// each platform. The storage tests and the pattern traversal tests
|
||||
// share this list, and one new attack name gets a test at each
|
||||
// location. The backslash cases guard the `\` half of the separator
|
||||
// check. That half is the Windows-only escape guard that a "simplify
|
||||
// to filepath.Base" refactor removes without a test failure. The colon,
|
||||
// reserved-name, and trailing dot and space cases guard the Windows
|
||||
// protections: NTFS alternate data streams, DOS device names, and name
|
||||
// suffixes that Windows removes.
|
||||
var invalidStorageNames = []string{
|
||||
"..", "../keep.txt", "/etc/passwd", "foo/../../keep.txt", ".", "",
|
||||
`foo\bar`, `..\x`,
|
||||
"foo:bar", "NUL", "con.txt", "CON.tar.gz", "foo.", "foo ",
|
||||
}
|
||||
|
||||
// newTraversalFixture returns a storage entity in a temporary root and
|
||||
// a marker file out of the entity directory. It also returns a check
|
||||
// that fails the test if the marker or the entity directory is gone.
|
||||
func newTraversalFixture(t *testing.T) (storage *StorageEntity, checkSurvived func()) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
dir := filepath.Join(root, "contexts")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
marker := filepath.Join(root, "keep.txt")
|
||||
if err := os.WriteFile(marker, []byte("keep"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
storage = &StorageEntity{Dir: dir, Label: "Contexts"}
|
||||
checkSurvived = func() {
|
||||
t.Helper()
|
||||
if _, err := os.Stat(marker); err != nil {
|
||||
t.Fatalf("parent marker was removed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
t.Fatalf("storage dir was removed: %v", err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestStorage_RejectsPathTraversal(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
|
||||
storage, checkSurvived := newTraversalFixture(t)
|
||||
for _, name := range invalidStorageNames {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := storage.Delete(name); err == nil {
|
||||
t.Fatalf("Delete(%q) succeeded, want error", name)
|
||||
}
|
||||
if err := storage.Save(name, []byte("pwned")); err == nil {
|
||||
t.Fatalf("Save(%q) succeeded, want error", name)
|
||||
}
|
||||
if _, err := storage.Load(name); err == nil {
|
||||
t.Fatalf("Load(%q) succeeded, want error", name)
|
||||
}
|
||||
if storage.Exists(name) {
|
||||
t.Fatalf("Exists(%q) is true, want false", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
checkSurvived()
|
||||
}
|
||||
|
||||
func TestInvalidStorageNameError_DefaultMessage(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
err := &InvalidStorageNameError{Name: "bad:name"}
|
||||
if got, want := err.Error(), `invalid name: "bad:name"`; got != want {
|
||||
t.Fatalf("Error() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_Rename(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
storage := &StorageEntity{Dir: dir, Label: "Contexts"}
|
||||
if err := storage.Save("old", []byte("content")); err != nil {
|
||||
t.Fatalf("failed to save content: %v", err)
|
||||
}
|
||||
if err := storage.Rename("old", "new"); err != nil {
|
||||
t.Fatalf("failed to rename: %v", err)
|
||||
}
|
||||
if storage.Exists("old") {
|
||||
t.Errorf("expected old name to be gone")
|
||||
}
|
||||
loaded, err := storage.Load("new")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load renamed content: %v", err)
|
||||
}
|
||||
if string(loaded) != "content" {
|
||||
t.Errorf("expected %q, got %q", "content", string(loaded))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_RenameRejectsPathTraversal(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
|
||||
storage, checkSurvived := newTraversalFixture(t)
|
||||
if err := storage.Save("ok", []byte("content")); err != nil {
|
||||
t.Fatalf("failed to save content: %v", err)
|
||||
}
|
||||
|
||||
for _, name := range invalidStorageNames {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := storage.Rename("ok", name); err == nil {
|
||||
t.Fatalf("Rename(%q, %q) succeeded, want error", "ok", name)
|
||||
}
|
||||
if err := storage.Rename(name, "ok"); err == nil {
|
||||
t.Fatalf("Rename(%q, %q) succeeded, want error", name, "ok")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
checkSurvived()
|
||||
if !storage.Exists("ok") {
|
||||
t.Fatalf("legitimate entry was moved or deleted")
|
||||
}
|
||||
}
|
||||
|
||||
// mustSymlink makes a symlink. If symlinks are not available, for
|
||||
// example on Windows without the privilege, it skips the test.
|
||||
func mustSymlink(t *testing.T, target, link string) {
|
||||
t.Helper()
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Skipf("cannot create symlink: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_RejectsSymlinkEscape(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
dir := filepath.Join(root, "store")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outside := filepath.Join(root, "outside.txt")
|
||||
if err := os.WriteFile(outside, []byte("keep"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustSymlink(t, outside, filepath.Join(dir, "escape"))
|
||||
mustSymlink(t, filepath.Join(root, "missing.txt"), filepath.Join(dir, "dangling"))
|
||||
|
||||
storage := &StorageEntity{Dir: dir}
|
||||
for _, name := range []string{"escape", "dangling"} {
|
||||
if _, err := storage.Load(name); err == nil {
|
||||
t.Fatalf("Load(%q) through an outside symlink did not fail", name)
|
||||
}
|
||||
if err := storage.Save(name, []byte("pwned")); err == nil {
|
||||
t.Fatalf("Save(%q) through an outside symlink did not fail", name)
|
||||
}
|
||||
}
|
||||
if got, _ := os.ReadFile(outside); string(got) != "keep" {
|
||||
t.Fatalf("outside file was overwritten: %q", got)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "missing.txt")); err == nil {
|
||||
t.Fatal("dangling symlink target was created")
|
||||
}
|
||||
}
|
||||
|
||||
// Load and Save operate through a symlink that stays in the storage
|
||||
// directory, and through a storage directory that is a symlink.
|
||||
func TestStorage_AllowsInternalAndDirSymlinks(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
realDir := filepath.Join(root, "real")
|
||||
if err := os.MkdirAll(realDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
storage := &StorageEntity{Dir: realDir}
|
||||
if err := storage.Save("target", []byte("content")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustSymlink(t, filepath.Join(realDir, "target"), filepath.Join(realDir, "alias"))
|
||||
if got, err := storage.Load("alias"); err != nil || string(got) != "content" {
|
||||
t.Fatalf("Load through an internal symlink: got %q, err %v", got, err)
|
||||
}
|
||||
|
||||
linkDir := filepath.Join(root, "link")
|
||||
mustSymlink(t, realDir, linkDir)
|
||||
linked := &StorageEntity{Dir: linkDir}
|
||||
if got, err := linked.Load("target"); err != nil || string(got) != "content" {
|
||||
t.Fatalf("Load via a symlinked storage dir: got %q, err %v", got, err)
|
||||
}
|
||||
if err := linked.Save("new", []byte("x")); err != nil {
|
||||
t.Fatalf("Save via a symlinked storage dir: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,47 @@
|
|||
package restapi
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const APIKeyHeader = "X-API-Key"
|
||||
|
||||
// requireAPIKeyForBind rejects a non-loopback bind address that has no
|
||||
// API key. An empty or unspecified host binds each interface, and that
|
||||
// counts as non-loopback.
|
||||
func requireAPIKeyForBind(address, apiKey string) error {
|
||||
if apiKey != "" {
|
||||
return nil
|
||||
}
|
||||
host := address
|
||||
if h, _, err := net.SplitHostPort(address); err == nil {
|
||||
host = h
|
||||
}
|
||||
if host == "localhost" {
|
||||
return nil
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(i18n.T("server_api_key_required"), address)
|
||||
}
|
||||
|
||||
// APIKeyMiddleware validates API key for protected endpoints.
|
||||
// Swagger documentation endpoints (/swagger/*) are exempt from authentication
|
||||
// to allow users to browse and test the API documentation freely.
|
||||
func APIKeyMiddleware(apiKey string) gin.HandlerFunc {
|
||||
// Compare digests, not the raw values. ConstantTimeCompare returns
|
||||
// early when the lengths are different, and that shows the length of
|
||||
// the configured key.
|
||||
expectedKey := sha256.Sum256([]byte(apiKey))
|
||||
return func(c *gin.Context) {
|
||||
// Skip authentication for Swagger documentation endpoints
|
||||
// This allows public access to API docs even when authentication is enabled
|
||||
|
|
@ -28,7 +57,8 @@ func APIKeyMiddleware(apiKey string) gin.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
if headerApiKey != apiKey {
|
||||
headerKey := sha256.Sum256([]byte(headerApiKey))
|
||||
if subtle.ConstantTimeCompare(headerKey[:], expectedKey[:]) != 1 {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Wrong API Key"})
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,11 +73,23 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
|
|||
|
||||
if err := c.BindJSON(&request); err != nil {
|
||||
log.Printf("Error binding JSON: %v", err)
|
||||
c.Writer.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
setHSTS(c)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf(i18n.T("server_invalid_request_format"), err)})
|
||||
return
|
||||
}
|
||||
|
||||
for _, prompt := range request.Prompts {
|
||||
if rejectUnsafePatternName(c, prompt.PatternName) {
|
||||
return
|
||||
}
|
||||
if rejectInvalidStorageName(c, prompt.ContextName) {
|
||||
return
|
||||
}
|
||||
if rejectInvalidStorageName(c, prompt.SessionName) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add log to check received language field
|
||||
log.Printf("Received chat request - Language: '%s', Prompts: %d", request.Language, len(request.Prompts))
|
||||
|
||||
|
|
@ -100,6 +112,11 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
|
|||
i+1, prompt.Model, prompt.PatternName, prompt.ContextName)
|
||||
|
||||
streamChan := make(chan domain.StreamUpdate)
|
||||
// Send returns an error for a failure that happens before it starts
|
||||
// to stream, such as a pattern that needs a variable the request
|
||||
// does not give. Such an error never reaches streamChan, so keep it
|
||||
// here and report it after the stream ends.
|
||||
sendErrChan := make(chan error, 1)
|
||||
|
||||
go func(p PromptRequest) {
|
||||
defer close(streamChan)
|
||||
|
|
@ -135,11 +152,12 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
|
|||
_, err = chatter.Send(c.Request.Context(), chatReq, opts)
|
||||
if err != nil {
|
||||
log.Printf("Error from chatter.Send: %v", err)
|
||||
// Error already sent to streamChan via domain.StreamTypeError if occurred in Send loop
|
||||
sendErrChan <- err
|
||||
return
|
||||
}
|
||||
}(prompt)
|
||||
|
||||
sawError := false
|
||||
for update := range streamChan {
|
||||
select {
|
||||
case <-clientGone:
|
||||
|
|
@ -159,6 +177,7 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
|
|||
Usage: update.Usage,
|
||||
}
|
||||
case domain.StreamTypeError:
|
||||
sawError = true
|
||||
response = StreamResponse{
|
||||
Type: "error",
|
||||
Format: "plain",
|
||||
|
|
@ -173,6 +192,15 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// The goroutine writes sendErrChan before it closes streamChan, so
|
||||
// the value is here by the time the loop above ends.
|
||||
if response, ok := unreportedSendError(sendErrChan, sawError); ok {
|
||||
if err := writeSSEResponse(c.Writer, response); err != nil {
|
||||
log.Printf("Error writing error response: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
completeResponse := StreamResponse{
|
||||
Type: "complete",
|
||||
Format: "plain",
|
||||
|
|
@ -186,6 +214,32 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// unreportedSendError reads the error that Send returned and makes the response
|
||||
// that tells the client about it. The second result is false when there is
|
||||
// nothing to send.
|
||||
//
|
||||
// Send both gives a stream error to the update channel and returns it, so the
|
||||
// client already has that error and must not get it twice. The sawError
|
||||
// argument says whether an error went to the client during the stream. An error
|
||||
// that Send makes before or after the stream, such as a pattern that needs a
|
||||
// variable the request does not give, never goes to the update channel, and
|
||||
// this function is the only way the client learns about it.
|
||||
func unreportedSendError(sendErrChan <-chan error, sawError bool) (StreamResponse, bool) {
|
||||
select {
|
||||
case sendErr := <-sendErrChan:
|
||||
if sendErr == nil || sawError {
|
||||
return StreamResponse{}, false
|
||||
}
|
||||
return StreamResponse{
|
||||
Type: "error",
|
||||
Format: "plain",
|
||||
Content: fmt.Sprintf(i18n.T("server_chat_error"), sendErr),
|
||||
}, true
|
||||
default:
|
||||
return StreamResponse{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func buildPromptChatRequest(p PromptRequest, language string) *domain.ChatRequest {
|
||||
return &domain.ChatRequest{
|
||||
Message: &chat.ChatCompletionMessage{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package restapi
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildPromptChatRequest_PreservesStrategyAndUserInput(t *testing.T) {
|
||||
prompt := PromptRequest{
|
||||
|
|
@ -43,3 +46,72 @@ func TestBuildPromptChatRequest_PreservesStrategyAndUserInput(t *testing.T) {
|
|||
t.Fatalf("expected variables to be preserved, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnreportedSendError(t *testing.T) {
|
||||
patternErr := errors.New("could not get pattern write_essay: missing required variable: author_name")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sendErr error
|
||||
sawError bool
|
||||
wantReport bool
|
||||
wantContent string
|
||||
}{
|
||||
{
|
||||
// A pattern that needs a variable the request does not give fails
|
||||
// before Send starts to stream, so no error reached the client yet.
|
||||
// This is the only chance to tell the client about it.
|
||||
name: "error before the stream reaches the client",
|
||||
sendErr: patternErr,
|
||||
sawError: false,
|
||||
wantReport: true,
|
||||
wantContent: "Error: " + patternErr.Error(),
|
||||
},
|
||||
{
|
||||
// Send gives a stream error to the update channel and also returns
|
||||
// it. The client has it already, so a second report would show the
|
||||
// same failure twice.
|
||||
name: "error during the stream is not sent again",
|
||||
sendErr: errors.New("vendor stream failed"),
|
||||
sawError: true,
|
||||
wantReport: false,
|
||||
},
|
||||
{
|
||||
name: "no error and no report",
|
||||
sendErr: nil,
|
||||
sawError: false,
|
||||
wantReport: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
sendErrChan := make(chan error, 1)
|
||||
if test.sendErr != nil {
|
||||
sendErrChan <- test.sendErr
|
||||
}
|
||||
|
||||
response, ok := unreportedSendError(sendErrChan, test.sawError)
|
||||
|
||||
if ok != test.wantReport {
|
||||
t.Fatalf("want report=%v, got %v", test.wantReport, ok)
|
||||
}
|
||||
if !test.wantReport {
|
||||
return
|
||||
}
|
||||
if response.Type != "error" {
|
||||
t.Errorf("want type %q, got %q", "error", response.Type)
|
||||
}
|
||||
if response.Content != test.wantContent {
|
||||
t.Errorf("want content %q, got %q", test.wantContent, response.Content)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An empty channel must not block the response, which the client waits for.
|
||||
func TestUnreportedSendErrorWithEmptyChannel(t *testing.T) {
|
||||
if _, ok := unreportedSendError(make(chan error, 1), false); ok {
|
||||
t.Error("want no report from an empty channel")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,15 +34,26 @@ func (h *ModelsHandler) GetModelNames(c *gin.Context) {
|
|||
}
|
||||
|
||||
response := make(map[string]any)
|
||||
vendors := make(map[string][]string)
|
||||
response["models"] = h.getAllModelNames(vendorsModels)
|
||||
response["vendors"] = buildVendorsMap(vendorsModels)
|
||||
c.JSON(200, response)
|
||||
}
|
||||
|
||||
// buildVendorsMap groups the model names by vendor name for the response.
|
||||
// A vendor with no models gets an empty slice, not a nil slice, because a nil
|
||||
// slice becomes null in JSON and a client that reads the list of a vendor then
|
||||
// gets null in place of an array. Ollama does this when it is in the
|
||||
// configuration but serves no models.
|
||||
func buildVendorsMap(vendorsModels *ai.VendorsModels) map[string][]string {
|
||||
vendors := make(map[string][]string)
|
||||
for _, groupItems := range vendorsModels.GroupsItems {
|
||||
if groupItems.Items == nil {
|
||||
vendors[groupItems.Group] = []string{}
|
||||
continue
|
||||
}
|
||||
vendors[groupItems.Group] = groupItems.Items
|
||||
}
|
||||
|
||||
response["models"] = h.getAllModelNames(vendorsModels)
|
||||
response["vendors"] = vendors
|
||||
c.JSON(200, response)
|
||||
return vendors
|
||||
}
|
||||
|
||||
func (h *ModelsHandler) getAllModelNames(vendorsModels *ai.VendorsModels) []string {
|
||||
|
|
|
|||
48
internal/server/models_test.go
Normal file
48
internal/server/models_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package restapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/plugins/ai"
|
||||
)
|
||||
|
||||
func TestBuildVendorsMapGivesEmptyArrayForVendorWithoutModels(t *testing.T) {
|
||||
vendorsModels := ai.NewVendorsModels()
|
||||
vendorsModels.AddGroupItems("Anthropic", "claude-opus-5", "claude-sonnet-5")
|
||||
// A variadic call with no items makes a nil slice, which is what a vendor in
|
||||
// the configuration that serves no models produces.
|
||||
vendorsModels.AddGroupItems("Ollama")
|
||||
|
||||
vendors := buildVendorsMap(vendorsModels)
|
||||
|
||||
if got := vendors["Ollama"]; got == nil {
|
||||
t.Error("want an empty slice for a vendor with no models, got nil")
|
||||
}
|
||||
if got := len(vendors["Ollama"]); got != 0 {
|
||||
t.Errorf("want 0 models for Ollama, got %d", got)
|
||||
}
|
||||
if got := len(vendors["Anthropic"]); got != 2 {
|
||||
t.Errorf("want 2 models for Anthropic, got %d", got)
|
||||
}
|
||||
|
||||
// The client reads vendors[name] as an array. Confirm the JSON holds an
|
||||
// array and not null, which is what stopped the web UI from listing models.
|
||||
encoded, err := json.Marshal(vendors)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal failed: %v", err)
|
||||
}
|
||||
var decoded map[string][]string
|
||||
if err = json.Unmarshal(encoded, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal failed: %v", err)
|
||||
}
|
||||
if decoded["Ollama"] == nil {
|
||||
t.Errorf("want an array for Ollama in JSON, got null: %s", encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildVendorsMapWithNoVendors(t *testing.T) {
|
||||
if got := len(buildVendorsMap(ai.NewVendorsModels())); got != 0 {
|
||||
t.Errorf("want an empty map, got %d entries", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
|
@ -45,6 +46,7 @@ type APIConvert struct {
|
|||
registry *core.PluginRegistry
|
||||
r *gin.Engine
|
||||
addr *string
|
||||
apiKey string
|
||||
}
|
||||
|
||||
type OllamaRequestBody struct {
|
||||
|
|
@ -188,12 +190,49 @@ func parseOllamaNumCtx(options map[string]any) (int, error) {
|
|||
return contextLength, nil
|
||||
}
|
||||
|
||||
func ServeOllama(registry *core.PluginRegistry, address string, version string) (err error) {
|
||||
// fabricChatClient sends the /api/chat self-forward, which can contain
|
||||
// the configured API key. It does not use a proxy, because the default
|
||||
// transport obeys HTTP_PROXY and can send the key to the proxy. It does
|
||||
// not obey redirects, and cannot send the key again to a location that
|
||||
// the operator did not configure.
|
||||
var fabricChatClient = newFabricChatClient()
|
||||
|
||||
func newFabricChatClient() *http.Client {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = nil
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ServeOllama operates the Ollama-compatible API server on address. An
|
||||
// empty apiKey sets authentication to off. This is permitted only for
|
||||
// loopback binds.
|
||||
func ServeOllama(registry *core.PluginRegistry, address string, version string, apiKey string) error {
|
||||
if err := requireAPIKeyForBind(address, apiKey); err != nil {
|
||||
return err
|
||||
}
|
||||
return newOllamaEngine(registry, address, version, apiKey).Run(address)
|
||||
}
|
||||
|
||||
// newOllamaEngine makes the engine but does not start it, which lets
|
||||
// tests operate the routes. The address parameter is the /api/chat
|
||||
// forward target, not the listen address that Run gets. In production
|
||||
// the two are the same value.
|
||||
func newOllamaEngine(registry *core.PluginRegistry, address string, version string, apiKey string) *gin.Engine {
|
||||
r := gin.New()
|
||||
|
||||
// Middleware
|
||||
r.Use(gin.Logger())
|
||||
r.Use(gin.Recovery())
|
||||
if apiKey != "" {
|
||||
r.Use(APIKeyMiddleware(apiKey))
|
||||
} else {
|
||||
slog.Warn("Starting Ollama-compatible API server without API key authentication. This may pose security risks.")
|
||||
}
|
||||
|
||||
// Register routes
|
||||
fabricDb := registry.Db
|
||||
|
|
@ -208,6 +247,7 @@ func ServeOllama(registry *core.PluginRegistry, address string, version string)
|
|||
registry: registry,
|
||||
r: r,
|
||||
addr: &address,
|
||||
apiKey: apiKey,
|
||||
}
|
||||
// Ollama Endpoints
|
||||
r.GET("/api/tags", typeConversion.ollamaTags)
|
||||
|
|
@ -216,13 +256,7 @@ func ServeOllama(registry *core.PluginRegistry, address string, version string)
|
|||
})
|
||||
r.POST("/api/chat", typeConversion.ollamaChat)
|
||||
|
||||
// Start server
|
||||
err = r.Run(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
return r
|
||||
}
|
||||
|
||||
func (f APIConvert) ollamaTags(c *gin.Context) {
|
||||
|
|
@ -267,7 +301,7 @@ func (f APIConvert) ollamaChat(c *gin.Context) {
|
|||
err = json.Unmarshal(body, &prompt)
|
||||
if err != nil {
|
||||
log.Printf(i18n.T("ollama_error_unmarshalling_body"), err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.T("ollama_error_endpoint")})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.T("ollama_invalid_request_body")})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -335,14 +369,14 @@ func (f APIConvert) ollamaChat(c *gin.Context) {
|
|||
fabricChatReq, err := json.Marshal(chat)
|
||||
if err != nil {
|
||||
log.Printf(i18n.T("ollama_error_marshalling_body"), err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.T("ollama_failed_create_request")})
|
||||
return
|
||||
}
|
||||
var req *http.Request
|
||||
baseURL, err := buildFabricChatURL(*f.addr)
|
||||
if err != nil {
|
||||
log.Printf(i18n.T("ollama_error_building_chat_url"), err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.T("ollama_failed_create_request")})
|
||||
return
|
||||
}
|
||||
req, err = http.NewRequest("POST", fmt.Sprintf("%s/chat", baseURL), bytes.NewBuffer(fabricChatReq))
|
||||
|
|
@ -351,13 +385,16 @@ func (f APIConvert) ollamaChat(c *gin.Context) {
|
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.T("ollama_failed_create_request")})
|
||||
return
|
||||
}
|
||||
if f.apiKey != "" {
|
||||
req.Header.Set(APIKeyHeader, f.apiKey)
|
||||
}
|
||||
|
||||
req = req.WithContext(c.Request.Context())
|
||||
|
||||
fabricRes, err := http.DefaultClient.Do(req)
|
||||
fabricRes, err := fabricChatClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf(i18n.T("ollama_error_getting_chat_body"), err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.T("ollama_upstream_request_failed")})
|
||||
return
|
||||
}
|
||||
defer fabricRes.Body.Close()
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@ package restapi
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/core"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestBuildFabricChatURL(t *testing.T) {
|
||||
|
|
@ -361,3 +368,193 @@ func TestParseOllamaNumCtx(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOllamaEngine_APIKeyWiring(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
registry := &core.PluginRegistry{Db: fsdb.NewDb(t.TempDir())}
|
||||
|
||||
getVersion := func(r *gin.Engine, key string) int {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/version", nil)
|
||||
if key != "" {
|
||||
req.Header.Set(APIKeyHeader, key)
|
||||
}
|
||||
r.ServeHTTP(w, req)
|
||||
return w.Code
|
||||
}
|
||||
|
||||
withKey := newOllamaEngine(registry, ":0", "test-version", "secret")
|
||||
if code := getVersion(withKey, ""); code != http.StatusUnauthorized {
|
||||
t.Fatalf("no key presented: got %d, want 401", code)
|
||||
}
|
||||
if code := getVersion(withKey, "secret"); code != http.StatusOK {
|
||||
t.Fatalf("valid key presented: got %d, want 200", code)
|
||||
}
|
||||
|
||||
withoutKey := newOllamaEngine(registry, ":0", "test-version", "")
|
||||
if code := getVersion(withoutKey, ""); code != http.StatusOK {
|
||||
t.Fatalf("no key configured: got %d, want 200", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllamaChat_ForwardsAPIKeyToChat(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
// Make the loopback /chat route with the middleware installed, the
|
||||
// same as newOllamaEngine makes it when --api-key is set.
|
||||
upstream := gin.New()
|
||||
upstream.Use(APIKeyMiddleware("secret"))
|
||||
upstream.POST("/chat", func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||
fmt.Fprint(c.Writer, "data: {\"type\":\"content\",\"format\":\"markdown\",\"content\":\"hi\"}\n\n")
|
||||
})
|
||||
server := httptest.NewServer(upstream)
|
||||
defer server.Close()
|
||||
|
||||
chatRequest := func(key string) int {
|
||||
r := gin.New()
|
||||
conv := APIConvert{addr: &server.URL, apiKey: key}
|
||||
r.POST("/api/chat", conv.ollamaChat)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"model":"test:latest","messages":[{"role":"user","content":"hi"}]}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/chat", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
return w.Code
|
||||
}
|
||||
|
||||
if code := chatRequest("secret"); code != http.StatusOK {
|
||||
t.Fatalf("matching key: got %d, want 200", code)
|
||||
}
|
||||
if code := chatRequest("wrong"); code != http.StatusUnauthorized {
|
||||
t.Fatalf("wrong key: got %d, want 401", code)
|
||||
}
|
||||
}
|
||||
|
||||
// The self-forward client must not use a proxy. A proxy gets the
|
||||
// configured API key, and the operator did not configure that host.
|
||||
// The redirect test below covers the no-redirect property.
|
||||
func TestFabricChatClient_NoProxy(t *testing.T) {
|
||||
transport, ok := fabricChatClient.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
t.Fatalf("transport is %T, want *http.Transport", fabricChatClient.Transport)
|
||||
}
|
||||
if transport.Proxy != nil {
|
||||
t.Fatal("self-forward transport has a proxy configured")
|
||||
}
|
||||
}
|
||||
|
||||
// An upstream redirect must show as an upstream error. The client must
|
||||
// not go to the redirect target with the API key.
|
||||
func TestOllamaChat_DoesNotFollowUpstreamRedirect(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
redirectTargetHit := false
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
redirectTargetHit = true
|
||||
}))
|
||||
defer target.Close()
|
||||
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, target.URL+"/chat", http.StatusFound)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
r := gin.New()
|
||||
conv := APIConvert{addr: &upstream.URL, apiKey: "secret"}
|
||||
r.POST("/api/chat", conv.ollamaChat)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"model":"test:latest","messages":[{"role":"user","content":"hi"}]}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/chat", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if redirectTargetHit {
|
||||
t.Fatal("the redirect target was contacted")
|
||||
}
|
||||
if w.Code != http.StatusFound {
|
||||
t.Fatalf("got %d, want the upstream 302 surfaced as an error", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Malformed client JSON is a client error. The answer is a 400 with a
|
||||
// stable generic message, not a 500.
|
||||
func TestOllamaChat_MalformedJSONIs400(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
addr := ":0"
|
||||
r := gin.New()
|
||||
conv := APIConvert{addr: &addr}
|
||||
r.POST("/api/chat", conv.ollamaChat)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/chat", strings.NewReader("{not json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("got %d, want 400", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// An upstream that is not available is a 500. The body must not contain
|
||||
// the raw transport error, because that error shows the internal
|
||||
// upstream URL.
|
||||
func TestOllamaChat_UpstreamFailureHidesDetails(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
server := httptest.NewServer(http.NotFoundHandler())
|
||||
url := server.URL
|
||||
server.Close() // nothing listens on url anymore
|
||||
|
||||
r := gin.New()
|
||||
conv := APIConvert{addr: &url}
|
||||
r.POST("/api/chat", conv.ollamaChat)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"model":"test:latest","messages":[{"role":"user","content":"hi"}]}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/chat", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("got %d, want 500", w.Code)
|
||||
}
|
||||
got := w.Body.String()
|
||||
if strings.Contains(got, "dial tcp") || strings.Contains(got, strings.TrimPrefix(url, "http://")) {
|
||||
t.Fatalf("500 body leaks transport details: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// With no configured key, the forwarded request must not contain the header.
|
||||
func TestOllamaChat_NoKeyOmitsHeader(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
var gotHeader string
|
||||
upstream := gin.New()
|
||||
upstream.POST("/chat", func(c *gin.Context) {
|
||||
gotHeader = c.GetHeader(APIKeyHeader)
|
||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||
fmt.Fprint(c.Writer, "data: {\"type\":\"content\",\"format\":\"markdown\",\"content\":\"hi\"}\n\n")
|
||||
})
|
||||
server := httptest.NewServer(upstream)
|
||||
defer server.Close()
|
||||
|
||||
r := gin.New()
|
||||
conv := APIConvert{addr: &server.URL, apiKey: ""}
|
||||
r.POST("/api/chat", conv.ollamaChat)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"model":"test:latest","messages":[{"role":"user","content":"hi"}]}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/chat", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("got %d, want 200", w.Code)
|
||||
}
|
||||
if gotHeader != "" {
|
||||
t.Fatalf("X-API-Key was forwarded with no key configured: %q", gotHeader)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
330
internal/server/path_traversal_test.go
Normal file
330
internal/server/path_traversal_test.go
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
package restapi
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Each storage route that gets a name must reject a traversal name, not
|
||||
// only DELETE. Most routes share storageError through the fsdb layer.
|
||||
// The exists route validates in the handler, because its storage
|
||||
// contract returns only a bool.
|
||||
func TestStorageHandler_RejectsTraversalOnAllRoutes(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
root := t.TempDir()
|
||||
contextsDir := filepath.Join(root, "contexts")
|
||||
if err := os.MkdirAll(contextsDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
marker := filepath.Join(root, "keep.txt")
|
||||
if err := os.WriteFile(marker, []byte("keep"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
NewContextsHandler(r, &fsdb.ContextsEntity{
|
||||
StorageEntity: &fsdb.StorageEntity{Label: "Contexts", Dir: contextsDir},
|
||||
})
|
||||
|
||||
for _, tc := range []struct{ method, path string }{
|
||||
{http.MethodGet, "/contexts/%2e%2e"},
|
||||
{http.MethodDelete, "/contexts/%2e%2e"},
|
||||
{http.MethodDelete, "/contexts/.."},
|
||||
{http.MethodPost, "/contexts/%2e%2e"},
|
||||
{http.MethodPut, "/contexts/rename/%2e%2e/ok"},
|
||||
{http.MethodPut, "/contexts/rename/ok/%2e%2e"},
|
||||
{http.MethodGet, "/contexts/exists/%2e%2e"},
|
||||
} {
|
||||
var body io.Reader
|
||||
if tc.method == http.MethodPost {
|
||||
body = strings.NewReader("x")
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(tc.method, tc.path, body)
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("%s %s: got %d, want 400", tc.method, tc.path, w.Code)
|
||||
}
|
||||
if w.Header().Get("Strict-Transport-Security") == "" {
|
||||
t.Fatalf("%s %s: validation 400 lacks the HSTS header", tc.method, tc.path)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(marker); err != nil {
|
||||
t.Fatalf("parent marker was deleted: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(contextsDir); err != nil {
|
||||
t.Fatalf("contexts dir was deleted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A non-validation failure stays a 500. Its body must not show the
|
||||
// filesystem path that is in the wrapped *os.PathError.
|
||||
func TestStorageHandler_GenericErrorHidesPaths(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
dir := t.TempDir()
|
||||
r := gin.New()
|
||||
NewContextsHandler(r, &fsdb.ContextsEntity{
|
||||
StorageEntity: &fsdb.StorageEntity{Label: "Contexts", Dir: dir},
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/contexts/missing", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("got %d, want 500", w.Code)
|
||||
}
|
||||
if strings.Contains(w.Body.String(), dir) {
|
||||
t.Fatalf("500 body leaks the storage path: %s", w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "internal error") {
|
||||
t.Fatalf("500 body is not the generic message: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// A pattern backend failure must use the shared storageError mapping.
|
||||
// That is a JSON envelope with the generic message, never the wrapped
|
||||
// os.PathError.
|
||||
func TestPatternsHandler_BackendErrorHidesPaths(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
dir := t.TempDir()
|
||||
r := gin.New()
|
||||
NewPatternsHandler(r, &fsdb.PatternsEntity{
|
||||
StorageEntity: &fsdb.StorageEntity{Label: "Patterns", Dir: dir, ItemIsDir: true},
|
||||
SystemPatternFile: "system.md",
|
||||
})
|
||||
|
||||
for _, req := range []*http.Request{
|
||||
httptest.NewRequest(http.MethodGet, "/patterns/missing", nil),
|
||||
httptest.NewRequest(http.MethodPost, "/patterns/missing/apply", strings.NewReader(`{"input":"x"}`)),
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("%s %s: got %d, want 500", req.Method, req.URL.Path, w.Code)
|
||||
}
|
||||
if strings.Contains(w.Body.String(), dir) {
|
||||
t.Fatalf("%s %s: 500 body leaks the storage path: %s", req.Method, req.URL.Path, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "internal error") {
|
||||
t.Fatalf("%s %s: 500 body is not the generic envelope: %s", req.Method, req.URL.Path, w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternsHandler_RejectsPathTraversalSave(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
root := t.TempDir()
|
||||
patternsDir := filepath.Join(root, "patterns")
|
||||
if err := os.MkdirAll(patternsDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
NewPatternsHandler(r, &fsdb.PatternsEntity{
|
||||
StorageEntity: &fsdb.StorageEntity{Label: "Patterns", Dir: patternsDir, ItemIsDir: true},
|
||||
SystemPatternFile: "system.md",
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/patterns/%2e%2e", strings.NewReader("pwned"))
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("POST /patterns/%%2e%%2e: got %d, want 400", w.Code)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "system.md")); err == nil {
|
||||
t.Fatal("wrote system.md in the parent directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatHandler_RejectsUnsafeNames(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
// The zero-value handler is an intentional seam. A request that goes
|
||||
// through pre-validation causes a nil panic in HandleChat. These
|
||||
// tests cannot pass on a request that got no validation.
|
||||
r := gin.New()
|
||||
r.POST("/chat", (&ChatHandler{}).HandleChat)
|
||||
|
||||
cases := map[string]string{
|
||||
// Every prefix class loadPattern treats as a filesystem path
|
||||
"absolute pattern name": `{"prompts":[{"patternName":"/etc/hosts","userInput":"x"}]}`,
|
||||
"home pattern name": `{"prompts":[{"patternName":"~/secret.md","userInput":"x"}]}`,
|
||||
"relative pattern name": `{"prompts":[{"patternName":"./secret.md","userInput":"x"}]}`,
|
||||
"backslash pattern name": `{"prompts":[{"patternName":"\\secret.md","userInput":"x"}]}`,
|
||||
// Context and session names get the same pre-validation
|
||||
"traversal context name": `{"prompts":[{"userInput":"x","contextName":"../keep.txt"}]}`,
|
||||
"traversal session name": `{"prompts":[{"userInput":"x","sessionName":".."}]}`,
|
||||
// The rejection loop stops at the first bad name, at each depth
|
||||
"second prompt path-like": `{"prompts":[{"userInput":"x"},{"patternName":"/etc/hosts","userInput":"y"}]}`,
|
||||
}
|
||||
for name, body := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/chat", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("got status %d, want 400", w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternsHandler_RejectsUnsafeNamesOnReadRoutes(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
r := gin.New()
|
||||
NewPatternsHandler(r, &fsdb.PatternsEntity{
|
||||
StorageEntity: &fsdb.StorageEntity{Label: "Patterns", Dir: t.TempDir(), ItemIsDir: true},
|
||||
SystemPatternFile: "system.md",
|
||||
})
|
||||
|
||||
for _, req := range []*http.Request{
|
||||
httptest.NewRequest(http.MethodGet, "/patterns/%2e%2e", nil),
|
||||
httptest.NewRequest(http.MethodPost, "/patterns/%2e%2e/apply", strings.NewReader(`{"input":"x"}`)),
|
||||
// Names that fail only ValidateStorageName, not the file-path check
|
||||
httptest.NewRequest(http.MethodGet, "/patterns/foo:bar", nil),
|
||||
httptest.NewRequest(http.MethodGet, "/patterns/NUL", nil),
|
||||
httptest.NewRequest(http.MethodPost, "/patterns/foo:bar/apply", strings.NewReader(`{"input":"x"}`)),
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("%s %s: got %d, want 400", req.Method, req.URL.Path, w.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A safe request must go through pre-validation. With the zero-value
|
||||
// handler seam, a request that goes through causes a nil panic, and
|
||||
// Recovery changes that into a 500. A 400 shows that validation
|
||||
// rejected valid names.
|
||||
func TestChatHandler_AcceptsBenignNames(t *testing.T) {
|
||||
if _, err := i18n.Init("en"); err != nil {
|
||||
t.Fatalf("i18n.Init() error = %v", err)
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.POST("/chat", (&ChatHandler{}).HandleChat)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"prompts":[{"userInput":"x","patternName":"summarize","contextName":"myctx","sessionName":"mysession"}]}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/chat", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code == http.StatusBadRequest {
|
||||
t.Fatalf("benign request was rejected with 400: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// A non-loopback bind without an API key must fail closed before the
|
||||
// server starts. A loopback bind operates without a key.
|
||||
func TestRequireAPIKeyForBind(t *testing.T) {
|
||||
tests := []struct {
|
||||
address string
|
||||
apiKey string
|
||||
wantErr bool
|
||||
}{
|
||||
{"127.0.0.1:8080", "", false},
|
||||
{"localhost:8080", "", false},
|
||||
{"localhost", "", false},
|
||||
{"[::1]:8080", "", false},
|
||||
{":8080", "", true}, // wildcard bind exposes every interface
|
||||
{"0.0.0.0:8080", "", true},
|
||||
{"[::]:8080", "", true},
|
||||
{"192.168.1.50:8080", "", true},
|
||||
{"example.com:8080", "", true},
|
||||
{":8080", "secret", false},
|
||||
{"0.0.0.0:8080", "secret", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.address, func(t *testing.T) {
|
||||
err := requireAPIKeyForBind(tt.address, tt.apiKey)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("requireAPIKeyForBind(%q, %q) error = %v, wantErr %v", tt.address, tt.apiKey, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Serve and ServeOllama must return the fail-closed error and must not
|
||||
// start an unauthenticated server on a non-loopback bind. The registry
|
||||
// is nil, and a check that does not occur first causes a panic.
|
||||
func TestServeFailsClosedOnNonLoopbackBind(t *testing.T) {
|
||||
if err := Serve(nil, ":0", ""); err == nil {
|
||||
t.Fatal("Serve on a wildcard bind without a key did not fail")
|
||||
}
|
||||
if err := ServeOllama(nil, ":0", "v", ""); err == nil {
|
||||
t.Fatal("ServeOllama on a wildcard bind without a key did not fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIKeyMiddleware(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(APIKeyMiddleware("secret"))
|
||||
r.GET("/ping", func(c *gin.Context) { c.Status(http.StatusOK) })
|
||||
|
||||
t.Run("missing key", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("got %d, want 401", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong key", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||
req.Header.Set(APIKeyHeader, "wrong")
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("got %d, want 401", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid key", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||
req.Header.Set(APIKeyHeader, "secret")
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("got %d, want 200", w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package restapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
|
@ -14,6 +16,22 @@ type PatternsHandler struct {
|
|||
patterns *fsdb.PatternsEntity
|
||||
}
|
||||
|
||||
// rejectUnsafePatternName answers a 400 when name is a file-path-like
|
||||
// pattern name or does not obey storage-name validation. An empty name
|
||||
// passes, because the chat handler guards prompt.PatternName, which is
|
||||
// optional.
|
||||
func rejectUnsafePatternName(c *gin.Context, name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
if fsdb.LooksLikePatternFilePath(name) || fsdb.ValidateStorageName(name) != nil {
|
||||
setHSTS(c)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf(i18n.T("pattern_invalid_name"), name)})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NewPatternsHandler creates a new PatternsHandler
|
||||
func NewPatternsHandler(r *gin.Engine, patterns *fsdb.PatternsEntity) (ret *PatternsHandler) {
|
||||
// Create a storage handler but don't register any routes yet
|
||||
|
|
@ -40,15 +58,19 @@ func NewPatternsHandler(r *gin.Engine, patterns *fsdb.PatternsEntity) (ret *Patt
|
|||
// @Produce json
|
||||
// @Param name path string true "Pattern name"
|
||||
// @Success 200 {object} fsdb.Pattern
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /patterns/{name} [get]
|
||||
func (h *PatternsHandler) Get(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if rejectUnsafePatternName(c, name) {
|
||||
return
|
||||
}
|
||||
|
||||
pattern, err := h.patterns.GetRaw(name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, err.Error())
|
||||
storageError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, pattern)
|
||||
|
|
@ -75,6 +97,9 @@ type PatternApplyRequest struct {
|
|||
// @Router /patterns/{name}/apply [post]
|
||||
func (h *PatternsHandler) ApplyPattern(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if rejectUnsafePatternName(c, name) {
|
||||
return
|
||||
}
|
||||
|
||||
var request PatternApplyRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
|
|
@ -93,7 +118,7 @@ func (h *PatternsHandler) ApplyPattern(c *gin.Context) {
|
|||
|
||||
pattern, err := h.patterns.GetApplyVariables(name, variables, request.Input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, err.Error())
|
||||
storageError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, pattern)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/core"
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
|
|
@ -27,6 +28,10 @@ import (
|
|||
// @in header
|
||||
// @name X-API-Key
|
||||
func Serve(registry *core.PluginRegistry, address string, apiKey string) (err error) {
|
||||
if err = requireAPIKeyForBind(address, apiKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
|
||||
// Middleware
|
||||
|
|
@ -36,7 +41,7 @@ func Serve(registry *core.PluginRegistry, address string, apiKey string) (err er
|
|||
if apiKey != "" {
|
||||
r.Use(APIKeyMiddleware(apiKey))
|
||||
} else {
|
||||
slog.Warn("Starting REST API server without API key authentication. This may pose security risks.")
|
||||
slog.Warn(i18n.T("server_no_api_key_warning"))
|
||||
}
|
||||
|
||||
// Swagger UI and documentation endpoint with custom YAML handler
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package restapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/plugins/db"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
|
@ -14,6 +17,42 @@ type StorageHandler[T any] struct {
|
|||
storage db.Storage[T]
|
||||
}
|
||||
|
||||
// setHSTS sets the Strict-Transport-Security header. Each validation
|
||||
// 400 sends it, the same as the chat BindJSON 400 path.
|
||||
func setHSTS(c *gin.Context) {
|
||||
c.Writer.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
}
|
||||
|
||||
// storageError answers err. A name-validation rejection becomes a 400,
|
||||
// and its body contains only the rejected name. All other errors stay
|
||||
// 500 errors with a generic body, because fsdb wraps *os.PathError
|
||||
// values and err.Error() then sends absolute filesystem paths to the
|
||||
// client. The full error goes to the log.
|
||||
func storageError(c *gin.Context, err error) {
|
||||
if _, ok := errors.AsType[*fsdb.InvalidStorageNameError](err); ok {
|
||||
setHSTS(c)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
slog.Error("storage operation failed", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
}
|
||||
|
||||
// rejectInvalidStorageName answers a 400 when name does not obey
|
||||
// storage-name validation. An empty name passes, because the fields
|
||||
// that this guards are optional.
|
||||
func rejectInvalidStorageName(c *gin.Context, name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
if err := fsdb.ValidateStorageName(name); err != nil {
|
||||
setHSTS(c)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NewStorageHandler creates a new StorageHandler
|
||||
func NewStorageHandler[T any](r *gin.Engine, entityType string, storage db.Storage[T]) (ret *StorageHandler[T]) {
|
||||
ret = &StorageHandler[T]{storage: storage}
|
||||
|
|
@ -31,7 +70,7 @@ func (h *StorageHandler[T]) Get(c *gin.Context) {
|
|||
name := c.Param("name")
|
||||
item, err := h.storage.Get(name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, err.Error())
|
||||
storageError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
|
|
@ -41,7 +80,7 @@ func (h *StorageHandler[T]) Get(c *gin.Context) {
|
|||
func (h *StorageHandler[T]) GetNames(c *gin.Context) {
|
||||
names, err := h.storage.GetNames()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, err.Error())
|
||||
storageError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, names)
|
||||
|
|
@ -52,15 +91,20 @@ func (h *StorageHandler[T]) Delete(c *gin.Context) {
|
|||
name := c.Param("name")
|
||||
err := h.storage.Delete(name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, err.Error())
|
||||
storageError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// Exists handles the GET /storage/exists/:name route
|
||||
// Exists handles the GET /storage/exists/:name route. The storage
|
||||
// Exists contract cannot report an invalid name, and the handler must
|
||||
// validate the name itself. An invalid name is a 400, not a "false".
|
||||
func (h *StorageHandler[T]) Exists(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if rejectInvalidStorageName(c, name) {
|
||||
return
|
||||
}
|
||||
exists := h.storage.Exists(name)
|
||||
c.JSON(http.StatusOK, exists)
|
||||
}
|
||||
|
|
@ -71,7 +115,7 @@ func (h *StorageHandler[T]) Rename(c *gin.Context) {
|
|||
newName := c.Param("newName")
|
||||
err := h.storage.Rename(oldName, newName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, err.Error())
|
||||
storageError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
|
|
@ -87,14 +131,14 @@ func (h *StorageHandler[T]) Save(c *gin.Context) {
|
|||
|
||||
content, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, err.Error())
|
||||
storageError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Save the content to storage
|
||||
err = h.storage.Save(name, content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, err.Error())
|
||||
storageError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
|
|
|
|||
|
|
@ -59,13 +59,6 @@ type PatternsLoader struct {
|
|||
|
||||
func (o *PatternsLoader) configure() (err error) {
|
||||
o.pathPatternsPrefix = fmt.Sprintf("%v/", o.DefaultFolder.Value)
|
||||
// Use a consistent temp folder name regardless of the source path structure
|
||||
tempDir, err := os.MkdirTemp("", "fabric-patterns-")
|
||||
if err != nil {
|
||||
return fmt.Errorf(i18n.T("patterns_failed_create_temp_folder"), err)
|
||||
}
|
||||
o.tempPatternsFolder = tempDir
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +89,15 @@ func (o *PatternsLoader) PopulateDB() (err error) {
|
|||
fmt.Println()
|
||||
fmt.Println()
|
||||
|
||||
// Create the temp folder here, not in configure(), so invocations that
|
||||
// do not download patterns do not leak an empty directory (issue #2190).
|
||||
var tempDir string
|
||||
if tempDir, err = os.MkdirTemp("", "fabric-patterns-"); err != nil {
|
||||
return fmt.Errorf(i18n.T("patterns_failed_create_temp_folder"), err)
|
||||
}
|
||||
o.tempPatternsFolder = tempDir
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
originalPath := o.DefaultFolder.Value
|
||||
if err = o.gitCloneAndCopy(); err != nil {
|
||||
return fmt.Errorf(i18n.T("patterns_failed_download_from_git"), err)
|
||||
|
|
|
|||
63
internal/tools/patterns_loader_test.go
Normal file
63
internal/tools/patterns_loader_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
|
||||
)
|
||||
|
||||
// Configure runs on every fabric invocation via the plugin registry. It must
|
||||
// not create the patterns temp directory; only PopulateDB uses it.
|
||||
func TestConfigureDoesNotCreateTempDir(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("TMPDIR", tmp)
|
||||
t.Setenv("TMP", tmp)
|
||||
t.Setenv("TEMP", tmp)
|
||||
|
||||
loader := NewPatternsLoader(&fsdb.PatternsEntity{
|
||||
StorageEntity: &fsdb.StorageEntity{Dir: t.TempDir()},
|
||||
})
|
||||
if err := loader.Configure(); err != nil {
|
||||
t.Fatalf("Configure() failed: %v", err)
|
||||
}
|
||||
|
||||
matches, err := filepath.Glob(filepath.Join(tmp, "fabric-patterns-*"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(matches) != 0 {
|
||||
t.Errorf("Configure() created temp directories: %v", matches)
|
||||
}
|
||||
}
|
||||
|
||||
// PopulateDB must create the temp directory lazily and remove it when done,
|
||||
// even on failure.
|
||||
func TestPopulateDBCleansUpTempDir(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("TMPDIR", tmp)
|
||||
t.Setenv("TMP", tmp)
|
||||
t.Setenv("TEMP", tmp)
|
||||
|
||||
loader := NewPatternsLoader(&fsdb.PatternsEntity{
|
||||
StorageEntity: &fsdb.StorageEntity{Dir: t.TempDir()},
|
||||
})
|
||||
if err := loader.Configure(); err != nil {
|
||||
t.Fatalf("Configure() failed: %v", err)
|
||||
}
|
||||
// Point at an invalid repo so PopulateDB fails fast without network.
|
||||
loader.DefaultGitRepoUrl.Value = filepath.Join(t.TempDir(), "no-such-repo")
|
||||
|
||||
if err := loader.PopulateDB(); err == nil {
|
||||
t.Fatal("PopulateDB() unexpectedly succeeded with invalid repo")
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
t.Errorf("PopulateDB() left temp entry behind: %v", e.Name())
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ schema = 3
|
|||
version = "v0.123.0"
|
||||
hash = "sha256-OjuvlqYrGvoUiXO/5ALbMxHvXRlrhDjLby1BN/nuhAw="
|
||||
[mod."cloud.google.com/go/auth"]
|
||||
version = "v0.22.0"
|
||||
hash = "sha256-EZmoz4OvycM9aIS1M3qexFo6ThMk0tESjdtq9mQytNQ="
|
||||
version = "v0.23.2"
|
||||
hash = "sha256-D28j2x5WFAg56/zTI9jGTH9MDUhteBo/RP1OKRKjB3s="
|
||||
[mod."cloud.google.com/go/auth/oauth2adapt"]
|
||||
version = "v0.2.8"
|
||||
hash = "sha256-GoXFqAbp1WO1tDj07PF5EyxDYvCBP0l0qwxY2oV2hfc="
|
||||
|
|
@ -17,8 +17,8 @@ schema = 3
|
|||
version = "v1.0.2"
|
||||
hash = "sha256-p6jdiHlLEfZES8vJnDywG4aVzIe16p0CU6iglglIweA="
|
||||
[mod."github.com/Azure/azure-sdk-for-go/sdk/azcore"]
|
||||
version = "v1.22.0"
|
||||
hash = "sha256-JhJgAeC3G0l+th1Xdu0Vf+FzWXTALHyb5FSIsKYTgBo="
|
||||
version = "v1.23.0"
|
||||
hash = "sha256-eOzqLjG50rNk/gZ264KPMNq0BaSuzH4saQBbvaWJpOQ="
|
||||
[mod."github.com/Azure/azure-sdk-for-go/sdk/azidentity"]
|
||||
version = "v1.14.0"
|
||||
hash = "sha256-oUoSbjql472sT24daGzICuBMEQLjpHXooGT83KtnUfc="
|
||||
|
|
@ -26,8 +26,8 @@ schema = 3
|
|||
version = "v1.12.0"
|
||||
hash = "sha256-jX/JR3WToz0vEWowfDBVjpul9EE2V2xGF1EfckrhDus="
|
||||
[mod."github.com/AzureAD/microsoft-authentication-library-for-go"]
|
||||
version = "v1.7.2"
|
||||
hash = "sha256-zB4M5bgMGDcjvF4GKu2t+NHTIHm9xQbsw9CUGT0tkk8="
|
||||
version = "v1.9.0"
|
||||
hash = "sha256-K29z7ruv8vG4PIz6O4DiXBenh/NAfySIrjb8ywl037A="
|
||||
[mod."github.com/KyleBanks/depth"]
|
||||
version = "v1.2.1"
|
||||
hash = "sha256-czR52MfeKA2FdStXCebTMQRKT8jaWQcbV214O3j49qU="
|
||||
|
|
@ -41,8 +41,8 @@ schema = 3
|
|||
version = "v1.3.4"
|
||||
hash = "sha256-CpXE/C+gW7V8MXREZguszzAQumizrCXGTdDRPBaflCs="
|
||||
[mod."github.com/anthropics/anthropic-sdk-go"]
|
||||
version = "v1.61.0"
|
||||
hash = "sha256-xAnuyZes72nZoanCzjjIbC22bcApU4mfVHGh3C1VhNg="
|
||||
version = "v1.67.0"
|
||||
hash = "sha256-mdEVuL2R8bhhrNAfAiHrvyew/cS0FTBtnsV/LFhC/LY="
|
||||
[mod."github.com/araddon/dateparse"]
|
||||
version = "v0.0.0-20210429162001-6b43995a97de"
|
||||
hash = "sha256-UuX84naeRGMsFOgIgRoBHG5sNy1CzBkWPKmd6VbLwFw="
|
||||
|
|
@ -50,77 +50,77 @@ schema = 3
|
|||
version = "v0.1.4"
|
||||
hash = "sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk="
|
||||
[mod."github.com/aws/aws-sdk-go-v2"]
|
||||
version = "v1.43.0"
|
||||
hash = "sha256-4aQxyJbnh6Swz+VDlsdtnX2yup5ou56OwS8EYHddmJE="
|
||||
version = "v1.44.0"
|
||||
hash = "sha256-0GXyEXecrEAKw1FxElG7rTPyXns5MpgvtEJFO8PSh+0="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream"]
|
||||
version = "v1.7.14"
|
||||
hash = "sha256-EJ3UrrMG10cTJLL8hIZ1SFR8O6J9rpdicL7x4RSk5CY="
|
||||
version = "v1.7.20"
|
||||
hash = "sha256-ICB2yD83Gl628LRkg3HhYlDtsL4xgGhOmphZ5D0bJXI="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/config"]
|
||||
version = "v1.32.31"
|
||||
hash = "sha256-D2jpMMLn5AgkXpZbbpab3+thofD6xCScixtRF2K32LU="
|
||||
version = "v1.32.40"
|
||||
hash = "sha256-aCTxFSDt/1hSgFSAAwHQzjSrOcSDg0lAt1ely0Eb6X4="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/credentials"]
|
||||
version = "v1.19.30"
|
||||
hash = "sha256-SoO465t2X7w3o7hGy88N1Zy6AEcG/LiFZ7xnPUmt7Jw="
|
||||
version = "v1.19.39"
|
||||
hash = "sha256-zPAy8z+s46o9En+df+FhYQCNNP58S+VLGAWyBIMHx5k="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/feature/ec2/imds"]
|
||||
version = "v1.18.31"
|
||||
hash = "sha256-6X1A4Cz7j40SaSUzS3u+QRbBzmHHZmValGBJeSHMQUo="
|
||||
version = "v1.18.40"
|
||||
hash = "sha256-LXOaz48PXTgjwbFYQ9cUc1Y9cmPODypl6SpZz9GLHZg="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/internal/configsources"]
|
||||
version = "v1.4.31"
|
||||
hash = "sha256-xv5BgkSZbTJwgDN8mtUCg3Q6Ac0S/IdPZW1tTrEsGgY="
|
||||
version = "v1.4.40"
|
||||
hash = "sha256-1EmqriwNg/7JCqpj7vnGB2oHgMrm/EgkLlGMhBTNh3k="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/internal/endpoints/v2"]
|
||||
version = "v2.7.31"
|
||||
hash = "sha256-dvD/UE5fVVxOb668bZNg5hM6H9F4foclI5HbD+b7Dks="
|
||||
version = "v2.7.40"
|
||||
hash = "sha256-XnMvLK1GmJcPNLov91EBKh+xpwIRk6DqlzXiI+epyd8="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/internal/v4a"]
|
||||
version = "v1.4.32"
|
||||
hash = "sha256-EovofAvyGZoaQsNwXl2F6Uc0+jNwRYV8At6if3Ki/z8="
|
||||
version = "v1.4.41"
|
||||
hash = "sha256-tgerq8bZzDemsJsVdBzHCEm/S2TyPpdSmG+uwCd9XJ0="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/bedrock"]
|
||||
version = "v1.66.0"
|
||||
hash = "sha256-xlv9OA8FmWK8Y1NuL9sarAvRQWP6iK0Vr/CsCkOCe2E="
|
||||
version = "v1.68.0"
|
||||
hash = "sha256-NKNT8mKBH3TDrCzH4WD1ukHEp+9tzojxrxpLXajiZ/8="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/bedrockruntime"]
|
||||
version = "v1.56.0"
|
||||
hash = "sha256-fNMQSKKnVasKUyEybcBlkP7/vCDM+cvyAgYhFwZ6Dyw="
|
||||
version = "v1.58.0"
|
||||
hash = "sha256-1vapY7IXJvcsWEn4rDaI8nSIfPCmuDHZPD9gP0DYHYA="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding"]
|
||||
version = "v1.13.13"
|
||||
hash = "sha256-C0L0B5qRP8IhX02Wz/wttNtOkI6xrI9VHQr/FiTCUsc="
|
||||
version = "v1.13.19"
|
||||
hash = "sha256-LfifyqFh49PPH0asF1TJz3QJYHTwxQttthUD5p4z2YM="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/internal/presigned-url"]
|
||||
version = "v1.13.31"
|
||||
hash = "sha256-43rStuRFoLjAAz/arhzCNsyxkX1DAlnQiKFbGv6AXSA="
|
||||
version = "v1.13.40"
|
||||
hash = "sha256-l88t3s/MssSelRkyukz4fYSs7mZCOvfCeNrsfzdyKuI="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/signin"]
|
||||
version = "v1.5.0"
|
||||
hash = "sha256-zty6NW++dIHGZiy4rBCkDLZVfDf9FSc1ouMeaUI42sc="
|
||||
version = "v1.6.0"
|
||||
hash = "sha256-NxIbOpqkwWKLSlZJx/dRoNMQ0QDh6jolC8xKJTxxo9A="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/sso"]
|
||||
version = "v1.33.0"
|
||||
hash = "sha256-1IpQJTXInKuQUXIX6pwxiMgwzJx9tO/n9ZEHn5vrkP0="
|
||||
version = "v1.34.0"
|
||||
hash = "sha256-yRdflsmsXNXki2xJLEtQur6Dpbb9N3beLdXD5Vu2Ss0="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/ssooidc"]
|
||||
version = "v1.38.0"
|
||||
hash = "sha256-FYqk0nJsW9FEMSjq9Rl9lOKtXvJ9AHAvsH0AZSohKbY="
|
||||
version = "v1.39.0"
|
||||
hash = "sha256-XGRs+nBRUfxyuCbDJ7QnV738UPMm2nSbxbLtK19hqY0="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/sts"]
|
||||
version = "v1.45.0"
|
||||
hash = "sha256-5CuJRPHxwORAl/YYeLFW9JSlT4J1AqDUaOSfqxG6vgE="
|
||||
version = "v1.46.0"
|
||||
hash = "sha256-7j2UmV1FHIcJ/K+iUhGGO2Q+dhuoHlOxR1TuCf4M68Y="
|
||||
[mod."github.com/aws/smithy-go"]
|
||||
version = "v1.27.4"
|
||||
hash = "sha256-V4BvRCkpbkBbFRKyxV+hGDPDKUd1nZYcr+WQ09GqC/k="
|
||||
version = "v1.28.1"
|
||||
hash = "sha256-52wJGnuZVxhZtqgp4Gf7mgYrK6DOL8hFgtjo8e1NlCA="
|
||||
[mod."github.com/bahlo/generic-list-go"]
|
||||
version = "v0.2.0"
|
||||
hash = "sha256-BIzqwG61hnMDknZOn/5+VX09yemzFzMjhPF48XoALto="
|
||||
[mod."github.com/buger/jsonparser"]
|
||||
version = "v1.2.0"
|
||||
hash = "sha256-CiwV8UFYw2PnnyUoRojiVLq+MlKJ/aviufLcRi1GLbw="
|
||||
version = "v1.6.1"
|
||||
hash = "sha256-barqoLEuh2FBMNG4kIjK3oRs8Izq4eVotpbzHPliqXc="
|
||||
[mod."github.com/bytedance/gopkg"]
|
||||
version = "v0.1.4"
|
||||
hash = "sha256-GpfG8K5YTWAt5MRg7RvpcemU7jfPV0A+mMbxSfA+2ws="
|
||||
[mod."github.com/bytedance/sonic"]
|
||||
version = "v1.15.2"
|
||||
hash = "sha256-FVhsfwSb5q7MzdVuPtElecrz8E62DJVfG2YI4mScuzk="
|
||||
version = "v1.15.3"
|
||||
hash = "sha256-/LTRsTbgpQPLEMX7CRIMxi/Unh9VAyys6QuALGWwgMc="
|
||||
[mod."github.com/bytedance/sonic/loader"]
|
||||
version = "v0.5.1"
|
||||
hash = "sha256-fbUguA0wNj+aggu3oHaEzkduQVO4oawcvjohfgUmha8="
|
||||
version = "v0.5.2"
|
||||
hash = "sha256-tsrtIbjYXZadwLzlL3ZmC4DEytwmT1tvPDQT/nwNb20="
|
||||
[mod."github.com/cespare/xxhash/v2"]
|
||||
version = "v2.3.0"
|
||||
hash = "sha256-7hRlwSR+fos1kx4VZmJ/7snR7zHh8ZFKX+qqqqGcQpY="
|
||||
[mod."github.com/cloudflare/circl"]
|
||||
version = "v1.6.4"
|
||||
hash = "sha256-HfbrlhDNcQ7shn2dBzUrXJRtRPMShjo+HiqquEwzByI="
|
||||
version = "v1.6.5"
|
||||
hash = "sha256-nrD+9cNt6xEohb0EA2bZCuSM03GXYPg/YMD0Y+HF8WU="
|
||||
[mod."github.com/cloudwego/base64x"]
|
||||
version = "v0.1.7"
|
||||
hash = "sha256-zhZGPCTM8HiLALQfaE/L7B0IwbXZxClDVLCFt5zbusE="
|
||||
|
|
@ -155,8 +155,8 @@ schema = 3
|
|||
version = "v5.9.1"
|
||||
hash = "sha256-VW8mfdLjvAuiooR2WV7P0IoOosAvtDZvFWcGQJJVSSY="
|
||||
[mod."github.com/go-git/go-git/v5"]
|
||||
version = "v5.19.1"
|
||||
hash = "sha256-C0e3oOXYRgLUhm1TQk9mBwRwwj6iusxFAt5U9vqWz8A="
|
||||
version = "v5.19.2"
|
||||
hash = "sha256-2du2hn5JDTEEYTWuGX+heSftkmd7HoaJXyWJG4bZeSM="
|
||||
[mod."github.com/go-logr/logr"]
|
||||
version = "v1.4.4"
|
||||
hash = "sha256-q9HX9aelONTLsywyLY4Wc5UYCK+wG0FbSzFWisCYiIA="
|
||||
|
|
@ -167,32 +167,32 @@ schema = 3
|
|||
version = "v1.0.0"
|
||||
hash = "sha256-he4+Ue27HuNS0imS55XaS4g54v2OrKFJppdhz8cO94s="
|
||||
[mod."github.com/go-openapi/jsonreference"]
|
||||
version = "v1.0.0"
|
||||
hash = "sha256-+UaHDX6kNBxmhH6N2BE22KyXISiEvxPyTpJF5i6l+GE="
|
||||
version = "v1.0.1"
|
||||
hash = "sha256-UMGGlJPWjL7SoeLd51PLyYnUuQMz1Jx41MuULCwQUp4="
|
||||
[mod."github.com/go-openapi/spec"]
|
||||
version = "v0.22.9"
|
||||
hash = "sha256-TJBuxXaAct9XElR6juOEa5jNSuiLNoMrWNscr3ygsEk="
|
||||
version = "v0.22.11"
|
||||
hash = "sha256-QmZOssbAZDUKdk/hZUmOxDYIa4u2VCIGm6E8/gY1y4Y="
|
||||
[mod."github.com/go-openapi/swag/conv"]
|
||||
version = "v0.27.3"
|
||||
hash = "sha256-8IoLDL3UsjWefUgvndFTevdeJhKhN1Qi8cm3j8NpH9U="
|
||||
version = "v0.29.1"
|
||||
hash = "sha256-hOh54OqE1yVfy5O2FRuHZZKejpUk26eM8qzB0ndoJhM="
|
||||
[mod."github.com/go-openapi/swag/jsonutils"]
|
||||
version = "v0.27.3"
|
||||
hash = "sha256-kksIySYFFusCXd+Iua+G2IjEfb+I1xCXlv7YH4E1/8o="
|
||||
version = "v0.29.1"
|
||||
hash = "sha256-Yj1UfqLEwJA5PWQTs3EbcohqxlknhFFwkYJXoO/Y6c0="
|
||||
[mod."github.com/go-openapi/swag/loading"]
|
||||
version = "v0.27.3"
|
||||
hash = "sha256-HPIc1YHoh06K1OExI6NnqsOKx5L+CzArTfXanTSYABk="
|
||||
version = "v0.29.1"
|
||||
hash = "sha256-ig2w4q11AOWerFTVN6a2MdZ4mKiFXWkeiTCDuWFwUxY="
|
||||
[mod."github.com/go-openapi/swag/pools"]
|
||||
version = "v0.27.3"
|
||||
hash = "sha256-v6e+U41LGxzT1aT4KV6PQvowY3N0Y6Z2zCItIpAuoG0="
|
||||
version = "v0.29.1"
|
||||
hash = "sha256-NhaFtD29d759M/H5AiYiHAxn6s4bkfYW63B7xFpkGvA="
|
||||
[mod."github.com/go-openapi/swag/stringutils"]
|
||||
version = "v0.27.3"
|
||||
hash = "sha256-QnCMIHYGwrMMzZBRFz3zvOoQWPgzToMIMRCfujww8Vw="
|
||||
version = "v0.29.1"
|
||||
hash = "sha256-VLkfHTvfO+yU6VMNdzSp3rb2v/A4OqpScBHq77GoOA8="
|
||||
[mod."github.com/go-openapi/swag/typeutils"]
|
||||
version = "v0.27.3"
|
||||
hash = "sha256-7DkJz+zFA2Zu6iIkoExf2H3gKKRg47KfzGE2aSXiehs="
|
||||
version = "v0.29.1"
|
||||
hash = "sha256-xsKC/xVt7LdZIHOv+h5KwJEULyebSFPTghUj1LC04uQ="
|
||||
[mod."github.com/go-openapi/swag/yamlutils"]
|
||||
version = "v0.27.3"
|
||||
hash = "sha256-dDH3tA5gTeifnUgq595ERiwWxyaoOpLpXsziXNxmC9U="
|
||||
version = "v0.29.1"
|
||||
hash = "sha256-nrru5csGEKnr+ff28sVdkJNubhjbLwY8cPp9JqSl1DE="
|
||||
[mod."github.com/go-playground/locales"]
|
||||
version = "v0.14.1"
|
||||
hash = "sha256-BMJGAexq96waZn60DJXZfByRHb8zA/JP/i6f/YrW9oQ="
|
||||
|
|
@ -239,11 +239,11 @@ schema = 3
|
|||
version = "v1.6.0"
|
||||
hash = "sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw="
|
||||
[mod."github.com/googleapis/enterprise-certificate-proxy"]
|
||||
version = "v0.3.19"
|
||||
hash = "sha256-I9lw9E0WvndUTWiwOlBQv6ySF6/+SS/cXeGLJnPlri4="
|
||||
version = "v0.3.21"
|
||||
hash = "sha256-h8t4jzU7hRY/KaSI51xAkxX2934i5k6XVvpuNPef4iE="
|
||||
[mod."github.com/googleapis/gax-go/v2"]
|
||||
version = "v2.23.0"
|
||||
hash = "sha256-aIJepJZI5PCsc9eteAP9x+n4RSSyOWi4kPnI5mmrBXk="
|
||||
version = "v2.24.0"
|
||||
hash = "sha256-uDcY93yvQS3wDkhM85bAxSo67Z+cWlwR1Vnv9hZRa5o="
|
||||
[mod."github.com/gorilla/websocket"]
|
||||
version = "v1.5.3"
|
||||
hash = "sha256-vTIGEFMEi+30ZdO6ffMNJ/kId6pZs5bbyqov8xe9BM0="
|
||||
|
|
@ -290,8 +290,8 @@ schema = 3
|
|||
version = "v0.0.24"
|
||||
hash = "sha256-pmq8KKIb2+qMF3E1cR2B7NAj5joF2tZduyXjIfIlkTk="
|
||||
[mod."github.com/mattn/go-sqlite3"]
|
||||
version = "v1.14.48"
|
||||
hash = "sha256-a2Cs9fotWnjY+9WaOnrHk6LbbP4idOHo//tAAlc6naI="
|
||||
version = "v1.14.50"
|
||||
hash = "sha256-olsWcNOIhMP67ZfuYvxxe3AHvyynAUmJ6WbDXfc1VbE="
|
||||
[mod."github.com/modern-go/concurrent"]
|
||||
version = "v0.0.0-20180306012644-bacd9c7ef1dd"
|
||||
hash = "sha256-OTySieAgPWR4oJnlohaFTeK1tRaVp/b0d1rYY8xKMzo="
|
||||
|
|
@ -302,8 +302,8 @@ schema = 3
|
|||
version = "v2.6.1"
|
||||
hash = "sha256-ag/8GBAwqkOyIVrdlaFYLxy9dgPOq7VbactrLmzxK7E="
|
||||
[mod."github.com/ollama/ollama"]
|
||||
version = "v0.32.3"
|
||||
hash = "sha256-giQRZODIilkGdEtyrzTMkBI1cUXMpVSZorv06U9aVA4="
|
||||
version = "v0.33.1"
|
||||
hash = "sha256-E297N5FElu4OEDM75FHekPr/fyxMpu+7M3GS3Robals="
|
||||
[mod."github.com/openai/openai-go"]
|
||||
version = "v1.12.0"
|
||||
hash = "sha256-JHLlKvDwERPf728GUXBsKU58ODgCxcxEe9TKJTGAG1w="
|
||||
|
|
@ -359,8 +359,8 @@ schema = 3
|
|||
version = "v0.0.1"
|
||||
hash = "sha256-ORbb8w6VS4Yw0vsPhVzAqCCUYyaMOPcQyyP4DBZgLzM="
|
||||
[mod."github.com/stretchr/testify"]
|
||||
version = "v1.11.1"
|
||||
hash = "sha256-sWfjkuKJyDllDEtnM8sb/pdLzPQmUYWYtmeWz/5suUc="
|
||||
version = "v1.12.1"
|
||||
hash = "sha256-9MTDdVjZMh1MJ5EH3HmAFrm24YqZayeMBgKwtaURZCc="
|
||||
[mod."github.com/swaggo/files"]
|
||||
version = "v1.0.1"
|
||||
hash = "sha256-bNBmpJaM7g1BNwd7VxNIRSdY35NKSXhYHGfnZsSEUZ8="
|
||||
|
|
@ -386,8 +386,8 @@ schema = 3
|
|||
version = "v0.15.1"
|
||||
hash = "sha256-HLk6oUe7EoITrNvP0y8D6BtIgIcmDZYtb/xl/dufIoY="
|
||||
[mod."github.com/ugorji/go/codec"]
|
||||
version = "v1.3.1"
|
||||
hash = "sha256-VQtXVaKxXjm5Q60hCgVKZxNywl6SJFPqju6JNjADp4w="
|
||||
version = "v1.3.2"
|
||||
hash = "sha256-/W4sowusGHO1Fb4mlqNQlfqgl278E0BqgMUzexkzWVw="
|
||||
[mod."github.com/wk8/go-ordered-map/v2"]
|
||||
version = "v2.1.8"
|
||||
hash = "sha256-v7/5+7lAypZfgClXgWxhxtA1skQq9o+1yrI+V0o1j2o="
|
||||
|
|
@ -395,44 +395,44 @@ schema = 3
|
|||
version = "v0.3.3"
|
||||
hash = "sha256-l3pGB6IdzcPA/HLk93sSN6NM2pKPy+bVOoacR5RC2+c="
|
||||
[mod."go.mongodb.org/mongo-driver/v2"]
|
||||
version = "v2.8.0"
|
||||
hash = "sha256-VaSRntfFY0SBfIPmhFVY0jPNlv0pYp8ivwdOoeLTeIs="
|
||||
version = "v2.8.2"
|
||||
hash = "sha256-1syL133Npfw250xMeOfEhExvEYFU7GTnWsdHCOOmNuA="
|
||||
[mod."go.opentelemetry.io/auto/sdk"]
|
||||
version = "v1.2.1"
|
||||
hash = "sha256-73bFYhnxNf4SfeQ52ebnwOWywdQbqc9lWawCcSgofvE="
|
||||
[mod."go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"]
|
||||
version = "v0.69.0"
|
||||
hash = "sha256-37oe5RsiBWxaOs7lZXJWEUN4P49jkr2ej+64FQ4ybT8="
|
||||
version = "v0.71.0"
|
||||
hash = "sha256-2vwtDspRM7jZyvxclBmKTCDZ31qw/TiG+HHI6Z8ub2s="
|
||||
[mod."go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"]
|
||||
version = "v0.69.0"
|
||||
hash = "sha256-ypZB41ebghWCNZmuoDgdGbWyTjCglBEirLZ+s7jqCgc="
|
||||
version = "v0.71.0"
|
||||
hash = "sha256-E5YtRLrs1/LqpvPt6LzC+sDpmxSROgpNmgca5s6iyCg="
|
||||
[mod."go.opentelemetry.io/otel"]
|
||||
version = "v1.44.0"
|
||||
hash = "sha256-XTGV9RjOIKyWrrgXJ32IA59lovxXqSeRrpcpC1FAFZA="
|
||||
version = "v1.46.0"
|
||||
hash = "sha256-2+xTacXn22hsJuMGGgXwazMbP/ZW5gkPRnf+rNUZgvQ="
|
||||
[mod."go.opentelemetry.io/otel/metric"]
|
||||
version = "v1.44.0"
|
||||
hash = "sha256-e8dVyRoavby/YG7aH2T05IhWHlLvwnMHyPhelAS1mCQ="
|
||||
version = "v1.46.0"
|
||||
hash = "sha256-cY1DXXzRfcP0kKh7oBzowbu6kbFr3jANiEVyMlD5Bt8="
|
||||
[mod."go.opentelemetry.io/otel/trace"]
|
||||
version = "v1.44.0"
|
||||
hash = "sha256-69HorWRTLLzAHRgLe4nPp13qJthb3qp7RU7c0Y9qV/c="
|
||||
version = "v1.46.0"
|
||||
hash = "sha256-zfj85gCR5Z3v1N+8nlOO+6+FVShaL/7Vb/3lXjdXnnw="
|
||||
[mod."go.yaml.in/yaml/v3"]
|
||||
version = "v3.0.4"
|
||||
hash = "sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4="
|
||||
version = "v3.0.5"
|
||||
hash = "sha256-ygho+GU5kE7vPMx+dZYyNfCaeMjNxj66XmrcVf3afFE="
|
||||
[mod."go.yaml.in/yaml/v4"]
|
||||
version = "v4.0.0-rc.6"
|
||||
hash = "sha256-9aOIQpQ53KLv/Hb14104qYKLVrZUcCGLwzYp6UGLP90="
|
||||
[mod."golang.org/x/arch"]
|
||||
version = "v0.29.0"
|
||||
hash = "sha256-CqC8aNJdS0Qtr31j2ao9hTQJk8KRfcHDL7/HL3mbD4g="
|
||||
version = "v0.30.0"
|
||||
hash = "sha256-od8lOmP+IOtiO7s1VaFVMBCQfRmtXT8jqe7Yo077xDs="
|
||||
[mod."golang.org/x/crypto"]
|
||||
version = "v0.54.0"
|
||||
hash = "sha256-m8jsqlPuoPxGDjClzcAVbXf1aiU07L+9j7vYcqJu/0Q="
|
||||
version = "v0.55.0"
|
||||
hash = "sha256-99uV/ESeqhaDEwqbXxDpePuwzO0JoEHGhqZXZVHq/H4="
|
||||
[mod."golang.org/x/mod"]
|
||||
version = "v0.38.0"
|
||||
hash = "sha256-BpKfvsmb7ecEDvTBKx9ZHauxoWRkOAZCqg4BrHM2/S0="
|
||||
version = "v0.40.0"
|
||||
hash = "sha256-gmuKtxidzYWVlQYNB/8BREmajyfockaQ5/6Xk4IR32A="
|
||||
[mod."golang.org/x/net"]
|
||||
version = "v0.57.0"
|
||||
hash = "sha256-gKo8UMw4hfETBHm8N5GOfuNadse9TWEQXJo2YWq5bY4="
|
||||
version = "v0.58.0"
|
||||
hash = "sha256-fDOkQJXURrbq2dBGa91bCUsXknCZlo3ru2JIofSOGRo="
|
||||
[mod."golang.org/x/oauth2"]
|
||||
version = "v0.36.0"
|
||||
hash = "sha256-evS7WkMrpgonmTcqtWFpC5rSKZN8O+vnAhNUs1MS9kw="
|
||||
|
|
@ -443,29 +443,29 @@ schema = 3
|
|||
version = "v0.47.0"
|
||||
hash = "sha256-TpbRyWWqHjddP6QzUgAbaLd2EE0S+GYNRUIDJd18r98="
|
||||
[mod."golang.org/x/text"]
|
||||
version = "v0.40.0"
|
||||
hash = "sha256-LJfnki46XEreGbSgjl+DeqgcTsINTOu2owyXNvijMcA="
|
||||
version = "v0.41.0"
|
||||
hash = "sha256-22nHcolG87qSPahT2Ey8S5iGlCLAglE9ObYXO6XZ3ZY="
|
||||
[mod."golang.org/x/time"]
|
||||
version = "v0.15.0"
|
||||
hash = "sha256-5D24A65wn7k93Jj3+918UKjB9ccmGHPBEqjD2XDB92E="
|
||||
[mod."golang.org/x/tools"]
|
||||
version = "v0.48.0"
|
||||
hash = "sha256-9cRNUaup6fexA5S1zc+Ic3aaoxAgKWntyMJ2xaOdiP8="
|
||||
version = "v0.49.0"
|
||||
hash = "sha256-3oaXp2PlfuIn6/UTarnYD279YqJKTyaCWT/KD7ABjD4="
|
||||
[mod."google.golang.org/api"]
|
||||
version = "v0.290.0"
|
||||
hash = "sha256-qqNG4ExSaHNa9iRHq/F4JXqdnegRrWFx8oMKEEMgApA="
|
||||
version = "v0.294.0"
|
||||
hash = "sha256-fSc8hIUo3a92Rf7CmI3D+gL5Vk33e5alr8EIIR613bc="
|
||||
[mod."google.golang.org/genai"]
|
||||
version = "v1.65.0"
|
||||
hash = "sha256-PtEwnslN9VBUur0fNwXLBryOzvw2zkHfHSlb0v/z+DQ="
|
||||
version = "v1.70.0"
|
||||
hash = "sha256-T0jXx52Hc3aOROPpX66RGhZs0FrRg5Bhqm3WyR1zjkM="
|
||||
[mod."google.golang.org/genproto/googleapis/rpc"]
|
||||
version = "v0.0.0-20260724162435-b2f20204f0df"
|
||||
hash = "sha256-ldJTTb7hhj1mdmzTn9IEkQVwCoj3KRlENZtUSEKHABU="
|
||||
version = "v0.0.0-20260825221802-da73d73af1c5"
|
||||
hash = "sha256-CQdjYGIrgAnEDfWW3DIJF9Orf7oJdhx3G9rGg6fow88="
|
||||
[mod."google.golang.org/grpc"]
|
||||
version = "v1.82.1"
|
||||
hash = "sha256-5Q85pZWiKulJSGRAjYcVrlBe7y4tfa9gsjCqYiBhLUU="
|
||||
version = "v1.83.2"
|
||||
hash = "sha256-q5VtdeJvWM7kbIvui8ncNV/+Rm+d0y/b6XkNN0/u1is="
|
||||
[mod."google.golang.org/protobuf"]
|
||||
version = "v1.36.11"
|
||||
hash = "sha256-7W+6jntfI/awWL3JP6yQedxqP5S9o3XvPgJ2XxxsIeE="
|
||||
version = "v1.36.12"
|
||||
hash = "sha256-MKd0AdkWEe2A79mGPzOY5I76nMqTsGJKQNPJOKyOwkQ="
|
||||
[mod."gopkg.in/warnings.v0"]
|
||||
version = "v0.1.2"
|
||||
hash = "sha256-ATVL9yEmgYbkJ1DkltDGRn/auGAjqGOfjQyBYyUo8s8="
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
"1.4.463"
|
||||
"1.4.478"
|
||||
|
|
|
|||
190
scripts/audit-patterns.sh
Executable file
190
scripts/audit-patterns.sh
Executable file
|
|
@ -0,0 +1,190 @@
|
|||
#!/usr/bin/env bash
|
||||
# audit-patterns.sh — Find maintenance issues in the Fabric patterns directory.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/audit-patterns.sh [--strict] [patterns_dir]
|
||||
#
|
||||
# --strict exit 1 when any issue is found (for CI); default is always exit 0
|
||||
# -h,--help show this help
|
||||
#
|
||||
# Checks:
|
||||
# 1. Thin patterns — system.md files under 15 lines (likely stubs)
|
||||
# 2. Bloated patterns — system.md files over 50 KB (likely embedded examples)
|
||||
# 3. Stale model refs — mentions of GPT-4, ChatGPT, or other vendor-specific models
|
||||
# 4. Missing INPUT marker — pattern has no "# INPUT" section
|
||||
# 5. Locale key gaps — i18n keys present in en.json but missing from other locales
|
||||
# 6. Completion gaps — flags in flags.go with no entry in completions files
|
||||
#
|
||||
# Output: plain text report. Exit 0 even when issues found (use --strict for non-zero exit).
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
STRICT=0
|
||||
PATTERNS_DIR=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--strict) STRICT=1; shift ;;
|
||||
-h|--help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
-*) echo "unknown option: $1 (try --help)" >&2; exit 2 ;;
|
||||
*) PATTERNS_DIR="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
PATTERNS_DIR="${PATTERNS_DIR:-$REPO_ROOT/data/patterns}"
|
||||
|
||||
if [[ ! -d "$PATTERNS_DIR" ]]; then
|
||||
echo "patterns directory not found: $PATTERNS_DIR" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Colorize only when writing to a terminal, so redirected output stays clean.
|
||||
if [[ -t 1 ]]; then
|
||||
YLW='\033[0;33m'
|
||||
GRN='\033[0;32m'
|
||||
DIM='\033[2m'
|
||||
RST='\033[0m'
|
||||
else
|
||||
YLW='' GRN='' DIM='' RST=''
|
||||
fi
|
||||
|
||||
# Print paths relative to the repo root instead of scripts/../data/...
|
||||
rel() { echo "${1#"$REPO_ROOT"/}"; }
|
||||
|
||||
issues=0
|
||||
|
||||
header() { echo; echo "━━━ $1 ━━━"; }
|
||||
warn() { echo -e " ${YLW}⚠${RST} $*"; issues=$((issues + 1)); }
|
||||
info() { echo -e " ${DIM}·${RST} $*"; }
|
||||
ok() { echo -e " ${GRN}✓${RST} $*"; }
|
||||
|
||||
# ── 1. Thin patterns ──────────────────────────────────────────────────────────
|
||||
header "Thin patterns (< 15 lines)"
|
||||
found=0
|
||||
while IFS= read -r f; do
|
||||
lines=$(wc -l < "$f")
|
||||
name=$(basename "$(dirname "$f")")
|
||||
if [[ $lines -lt 15 ]]; then
|
||||
warn "$name ($lines lines) → $(rel "$f")"
|
||||
found=1
|
||||
fi
|
||||
done < <(find "$PATTERNS_DIR" -name "system.md" | sort)
|
||||
[[ $found -eq 0 ]] && ok "None found"
|
||||
|
||||
# ── 2. Bloated patterns ───────────────────────────────────────────────────────
|
||||
header "Bloated patterns (> 50 KB — likely embedded examples)"
|
||||
found=0
|
||||
while IFS= read -r f; do
|
||||
size=$(wc -c < "$f")
|
||||
name=$(basename "$(dirname "$f")")
|
||||
if [[ $size -gt 51200 ]]; then
|
||||
kb=$(( size / 1024 ))
|
||||
warn "$name (${kb} KB) → $(rel "$f")"
|
||||
found=1
|
||||
fi
|
||||
done < <(find "$PATTERNS_DIR" -name "system.md" | sort)
|
||||
[[ $found -eq 0 ]] && ok "None found"
|
||||
|
||||
# ── 3. Stale model/vendor references ─────────────────────────────────────────
|
||||
header "Stale model/vendor references"
|
||||
STALE_PATTERN='GPT-4|GPT-3|gpt-4|gpt-3|ChatGPT|OpenAI'\''s|text-davinci|gpt4|gpt3|claude-[0-9]|gemini-pro'
|
||||
found=0
|
||||
while IFS= read -r f; do
|
||||
matches=$(grep -oE "$STALE_PATTERN" "$f" 2>/dev/null | sort -u | tr '\n' ' ')
|
||||
if [[ -n "$matches" ]]; then
|
||||
name=$(basename "$(dirname "$f")")
|
||||
warn "$name → found: ${matches% }"
|
||||
found=1
|
||||
fi
|
||||
done < <(find "$PATTERNS_DIR" -name "system.md" | sort)
|
||||
[[ $found -eq 0 ]] && ok "None found"
|
||||
|
||||
# ── 4. Missing INPUT marker ───────────────────────────────────────────────────
|
||||
header "Patterns missing # INPUT section"
|
||||
found=0
|
||||
while IFS= read -r f; do
|
||||
if ! grep -q "^# INPUT" "$f" 2>/dev/null; then
|
||||
name=$(basename "$(dirname "$f")")
|
||||
warn "$name → $(rel "$f")"
|
||||
found=1
|
||||
fi
|
||||
done < <(find "$PATTERNS_DIR" -name "system.md" | sort)
|
||||
[[ $found -eq 0 ]] && ok "None found"
|
||||
|
||||
# ── 5. i18n locale key gaps ───────────────────────────────────────────────────
|
||||
LOCALES_DIR="$REPO_ROOT/internal/i18n/locales"
|
||||
if [[ -d "$LOCALES_DIR" ]]; then
|
||||
header "i18n locale key gaps (keys in en.json missing from other locales)"
|
||||
found=0
|
||||
for locale_file in "$LOCALES_DIR"/*.json; do
|
||||
lang=$(basename "$locale_file" .json)
|
||||
[[ "$lang" == "en" ]] && continue
|
||||
missing=$(python3 -c "
|
||||
import json
|
||||
with open('$LOCALES_DIR/en.json') as f:
|
||||
en = set(json.load(f).keys())
|
||||
with open('$locale_file') as f:
|
||||
other = set(json.load(f).keys())
|
||||
missing = sorted(en - other)
|
||||
if missing:
|
||||
print('\n'.join(missing))
|
||||
" 2>/dev/null)
|
||||
if [[ -n "$missing" ]]; then
|
||||
count=$(echo "$missing" | wc -l)
|
||||
warn "$lang → $count missing key(s):"
|
||||
echo "$missing" | while IFS= read -r key; do
|
||||
info " $key"
|
||||
done
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
[[ $found -eq 0 ]] && ok "All locales in sync"
|
||||
fi
|
||||
|
||||
# ── 6. Shell completion gaps ──────────────────────────────────────────────────
|
||||
FLAGS_FILE="$REPO_ROOT/internal/cli/flags.go"
|
||||
BASH_FILE="$REPO_ROOT/completions/fabric.bash"
|
||||
FISH_FILE="$REPO_ROOT/completions/fabric.fish"
|
||||
ZSH_FILE="$REPO_ROOT/completions/_fabric"
|
||||
|
||||
if [[ -f "$FLAGS_FILE" ]]; then
|
||||
header "Shell completion gaps (long flags in flags.go missing from completions)"
|
||||
# Extract long flag names from struct tags: `long:"flag-name"`
|
||||
go_flags=$(grep -oP 'long:"[^"]+"' "$FLAGS_FILE" | sed 's/long:"//;s/"//' | sort -u)
|
||||
|
||||
found=0
|
||||
# bash and zsh use --flag-name; fish uses -l flag-name
|
||||
for comp_file in "$BASH_FILE" "$ZSH_FILE"; do
|
||||
[[ -f "$comp_file" ]] || continue
|
||||
comp_name=$(basename "$comp_file")
|
||||
while IFS= read -r flag; do
|
||||
if ! grep -qF -- "--$flag" "$comp_file" 2>/dev/null; then
|
||||
warn "$comp_name → missing --$flag"
|
||||
found=1
|
||||
fi
|
||||
done <<< "$go_flags"
|
||||
done
|
||||
if [[ -f "$FISH_FILE" ]]; then
|
||||
comp_name=$(basename "$FISH_FILE")
|
||||
while IFS= read -r flag; do
|
||||
# fish registers long options with: complete -c cmd -l flag-name
|
||||
if ! grep -qF -- "-l $flag" "$FISH_FILE" 2>/dev/null; then
|
||||
warn "$comp_name → missing -l $flag"
|
||||
found=1
|
||||
fi
|
||||
done <<< "$go_flags"
|
||||
fi
|
||||
[[ $found -eq 0 ]] && ok "All completions in sync"
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
echo
|
||||
echo "━━━ Summary ━━━"
|
||||
if [[ $issues -eq 0 ]]; then
|
||||
echo -e "${GRN}No issues found.${RST}"
|
||||
else
|
||||
echo -e "${YLW}${issues} issue(s) found.${RST}"
|
||||
fi
|
||||
|
||||
[[ $STRICT -eq 1 && $issues -gt 0 ]] && exit 1
|
||||
exit 0
|
||||
|
|
@ -39,13 +39,17 @@ docker run --rm -it -v $PWD/.env:/root/.config/fabric/.env fabric -p your-patter
|
|||
|
||||
## Running the server
|
||||
|
||||
Expose port 8080 to use Fabric's REST API:
|
||||
Expose port 8080 to use Fabric's REST API. In a container, bind all
|
||||
interfaces with `--address :8080` so the mapped port can reach the
|
||||
server, and set an API key, which is mandatory for non-loopback binds:
|
||||
|
||||
```bash
|
||||
docker run --rm -it -p 8080:8080 -v $HOME/.fabric-config:/root/.config/fabric fabric --serve
|
||||
docker run --rm -it -p 8080:8080 -v $HOME/.fabric-config:/root/.config/fabric \
|
||||
-e FABRIC_API_KEY=your-secret-key fabric --serve --address :8080
|
||||
```
|
||||
|
||||
The API will be available at `http://localhost:8080`.
|
||||
The API will be available at `http://localhost:8080`. Requests must send
|
||||
the key in the `X-API-Key` header.
|
||||
|
||||
## Multi-arch builds and GHCR packages
|
||||
|
||||
|
|
|
|||
|
|
@ -2108,6 +2108,15 @@
|
|||
"EXTRACT",
|
||||
"BUSINESS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"patternName": "generate_frontmatter",
|
||||
"description": "Generate YAML frontmatter with tags, aliases and summary for PKM notes.",
|
||||
"tags": [
|
||||
"WRITING",
|
||||
"EXTRACT",
|
||||
"CONVERSION"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1015,6 +1015,10 @@
|
|||
{
|
||||
"patternName": "extract_video_commerce_entities",
|
||||
"pattern_extract": "# IDENTITY and PURPOSE You are an expert at identifying every commercially relevant entity in a video transcript — the products shown, tools used, plants grown, books referenced, services mentioned, and brands displayed. You think like an affiliate manager reviewing content for placement opportunities. You understand that video content is uniquely rich with implicit product signals: a host reaches for a specific brand of pruners, uses a particular app on screen, wears a recognizable piece of gear. You surface all of it. Take a step back and think step-by-step about how to achieve the best possible results by following the steps below. # STEPS - Read the full transcript and extract all named or clearly implied commercial entities. - For each entity, record: - Name (exact as spoken, or brand inferred from description) - Category: tool / plant / material / book / course / service / software / apparel / food / other - Timestamp or approximate position (early / mid / late) if determinable from context - Mention type: explicit recommendation / casual use / on-screen / background / sponsored - Audience fit: how well this product matches what the video's audience would buy - Group entities by category. - Note any entities mentioned multiple times — repetition is a strong buying signal. - Identify the top 5 entities by purchase likelihood. # OUTPUT SECTIONS ## ENTITIES BY CATEGORY For each category with at least one entity: ### [Category Name] - `Name` | Mention type | Position | Audience fit (high/mid/low) ## REPEATED MENTIONS Entities mentioned more than once — strong conversion signal: - `Name` | Number of mentions | Why it matters ## TOP 5 PURCHASE CANDIDATES The entities most likely to drive a sale, ranked: 1. `Name` — [One sentence: why this audience buys this product] 2. ... ## CONTENT GAPS Needs the creator addressed where no product was named — affiliate placement opportunities: - `Need` | Suggested category # OUTPUT INSTRUCTIONS - Only output Markdown. - Do not output warnings or notes — only the requested sections. - If a section has no entries, write \"None identified.\" - Keep brand names exact. - Audience fit is relative to the video's topic and likely viewer — assess contextually. - Timestamp positions are approximate — use early (0-33%), mid (33-66%), late (66-100%) if exact times aren't determinable. # INPUT INPUT:"
|
||||
},
|
||||
{
|
||||
"patternName": "generate_frontmatter",
|
||||
"pattern_extract": "# IDENTITY and PURPOSE You are an expert at knowledge management and note metadata. Given any text — a document, article, essay, book chapter, transcript, meeting notes, or rough draft — you generate clean, well-structured YAML frontmatter suitable for personal knowledge management (PKM) systems such as Obsidian, Logseq, or any markdown-based notes vault. Your output is immediately paste-ready: valid YAML wrapped in `---` delimiters, placed at the top of the note. Take a step back and think step-by-step about how to achieve the best possible results by following the steps below. # STEPS - Read the entire input carefully to understand its content, type, and context. - Infer the most accurate and descriptive title if one is not explicitly present. - Identify the document type (article, chapter, meeting-notes, transcript, essay, reference, idea, etc.). - Extract 3–8 specific, lowercase tags that describe the content. Prefer concrete concepts over vague categories. Avoid single-word generic tags like \"notes\" or \"text\". - Generate 1–3 aliases: alternative titles or short names someone might search for. - Write a one-sentence summary (15–25 words) capturing the core argument or content. - Identify the author or source if present; leave blank if not. - Use today's date or the document date if detectable; otherwise omit the date field. - Note the document's primary domain or area (e.g., productivity, philosophy, technology, science, history). # OUTPUT Output ONLY the YAML frontmatter block. No explanation, no preamble, no commentary after the block. ```yaml --- title: \"Exact or inferred title\" aliases: - \"Short name\" - \"Alternative title\" tags: - specific-tag - another-tag - domain/subtopic type: article # article | chapter | transcript | meeting-notes | essay | reference | idea | book author: \"Author Name\" # omit if unknown source: \"\" # URL or citation if available; omit if not date: YYYY-MM-DD # omit if not determinable summary: \"One sentence capturing the core content or argument of this document.\" status: unprocessed # unprocessed | reading | processed | archived --- ``` # OUTPUT INSTRUCTIONS - Output ONLY the YAML block — nothing before `---` and nothing after the closing `---`. - Use lowercase for all tags. Use hyphens for multi-word tags (e.g., `knowledge-management`, not `KnowledgeManagement`). - For hierarchical tags use slash notation: `philosophy/stoicism`, `technology/ai`. - Be specific: `decision-making` is better than `thinking`; `ancient-rome` is better than `history`. - The summary must be a complete sentence, not a fragment. - Omit fields that cannot be reasonably inferred (author, source, date) rather than guessing. - Do not add any fields not shown in the template above. - Do not give warnings or notes; only output the YAML block. # INPUT INPUT:"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ export default tseslint.config(
|
|||
parserOptions: {
|
||||
parser: tseslint.parser
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// svelte-eslint-parser gives a `$:` reactive statement a variable
|
||||
// definition of type "ComputedVariable". The typescript-eslint rule
|
||||
// does not know that type and stops ESLint with a TypeError. Use the
|
||||
// core ESLint rule for Svelte components instead.
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'no-unused-vars': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
5569
web/package-lock.json
generated
5569
web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -16,37 +16,41 @@
|
|||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.3",
|
||||
"@skeletonlabs/skeleton": "^2.11.0",
|
||||
"@skeletonlabs/tw-plugin": "^0.3.1",
|
||||
"@eslint/js": "^9.39.5",
|
||||
"@firecrawl/pdf-inspector-wasm": "0.1.3",
|
||||
"@skeletonlabs/skeleton": "^5.0.0",
|
||||
"@skeletonlabs/skeleton-svelte": "^5.0.0",
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/kit": "^2.53.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"@sveltejs/kit": "^2.70.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^20.19.37",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@types/node": "^26.1.2",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.22.0",
|
||||
"globals": "^17.8.0",
|
||||
"lucide-svelte": "^0.575.0",
|
||||
"mdsvex": "^0.11.2",
|
||||
"mdsvex": "^0.12.8",
|
||||
"patch-package": "^8.0.1",
|
||||
"pdf-to-markdown-core": "github:jzillmann/pdf-to-markdown#modularize",
|
||||
"pdfjs-dist": "^5.5.207",
|
||||
"postcss": "^8.5.8",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"postcss": "^8.5.25",
|
||||
"prettier": "^3.9.6",
|
||||
"prettier-plugin-svelte": "^4.1.1",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"shiki": "^1.29.2",
|
||||
"svelte": "^5.53.7",
|
||||
"svelte-check": "^3.8.6",
|
||||
"shiki": "^4.4.1",
|
||||
"svelte": "^5.56.8",
|
||||
"svelte-check": "^4.7.4",
|
||||
"svelte-inview": "^4.0.4",
|
||||
"svelte-reveal": "^1.1.0",
|
||||
"svelte-youtube-embed": "^0.3.3",
|
||||
"svelte-youtube-lite": "^0.6.2",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"svelte-reveal": "^1.2.0",
|
||||
"svelte-youtube-embed": "^0.4.6",
|
||||
"svelte-youtube-lite": "^1.3.0",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.8",
|
||||
"vite-plugin-tailwind-purgecss": "^0.3.5"
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "^8.2.0",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"type": "module",
|
||||
"overrides": {
|
||||
|
|
@ -58,52 +62,24 @@
|
|||
"http-signature": ">=0.10.0",
|
||||
"mime": ">=1.4.1",
|
||||
"hoek": ">=4.2.1",
|
||||
"cookie": ">=0.7.0",
|
||||
"cookie": ">=0.7.0 <2.0.0",
|
||||
"tough-cookie": ">=4.1.3",
|
||||
"esbuild": ">=0.25.0",
|
||||
"@eslint/plugin-kit": ">=0.3.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.6",
|
||||
"@floating-ui/dom": "^1.8.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns": "^4.4.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"marked": "^15.0.12",
|
||||
"marked": "^18.0.7",
|
||||
"nanoid": "5.0.9",
|
||||
"rehype": "^13.0.2",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-unwrap-images": "^1.0.0",
|
||||
"tailwind-merge": "^2.6.1",
|
||||
"vfile-message": "^4.0.3",
|
||||
"yaml": "^2.8.3",
|
||||
"youtube-transcript": "^1.2.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"tunnel-agent@<0.6.0": ">=0.6.0",
|
||||
"qs@<6.0.4": ">=6.0.4",
|
||||
"qs@<1.0.0": ">=1.0.0",
|
||||
"qs@<6.14.1": ">=6.14.1",
|
||||
"hawk@<3.1.3": ">=3.1.3",
|
||||
"http-signature@<0.10.0": ">=0.10.0",
|
||||
"request@>=2.2.6 <2.47.0": ">=2.68.0",
|
||||
"mime@<1.4.1": ">=1.4.1",
|
||||
"hoek@<4.2.1": ">=4.2.1",
|
||||
"hawk@<9.0.1": ">=9.0.1",
|
||||
"qs@<6.2.4": ">=6.2.4",
|
||||
"cookie@<0.7.0": ">=0.7.0",
|
||||
"tough-cookie@<4.1.3": ">=4.1.3",
|
||||
"nanoid@<3.3.8": ">=3.3.8",
|
||||
"form-data@<2.5.4": ">=2.5.4",
|
||||
"glob@>=10.2.0 <10.5.0": ">=10.5.0",
|
||||
"esbuild@<=0.24.2": ">=0.25.0",
|
||||
"@eslint/plugin-kit@<0.3.4": ">=0.3.4"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"pdf-to-markdown-core",
|
||||
"svelte-preprocess"
|
||||
]
|
||||
"yaml": "^2.9.0",
|
||||
"youtube-transcript": "^1.3.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3737
web/pnpm-lock.yaml
3737
web/pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
48
web/pnpm-workspace.yaml
Normal file
48
web/pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# pnpm 11 and later versions do not read the "pnpm" field in package.json.
|
||||
# Keep pnpm settings in this file. See https://pnpm.io/settings
|
||||
#
|
||||
# The "overrides" field in package.json stays there for npm, which reads it
|
||||
# from package.json and writes package-lock.json.
|
||||
|
||||
# Each entry below corrects a vulnerable transitive dependency. Do not add
|
||||
# version pins for direct dependencies here. Set those in package.json, or the
|
||||
# two files can disagree.
|
||||
overrides:
|
||||
'tunnel-agent@<0.6.0': '>=0.6.0'
|
||||
'qs@<6.0.4': '>=6.0.4'
|
||||
'qs@<1.0.0': '>=1.0.0'
|
||||
'qs@<6.14.1': '>=6.14.1'
|
||||
'hawk@<3.1.3': '>=3.1.3'
|
||||
'http-signature@<0.10.0': '>=0.10.0'
|
||||
'request@>=2.2.6 <2.47.0': '>=2.68.0'
|
||||
'mime@<1.4.1': '>=1.4.1'
|
||||
'hoek@<4.2.1': '>=4.2.1'
|
||||
'hawk@<9.0.1': '>=9.0.1'
|
||||
'qs@<6.2.4': '>=6.2.4'
|
||||
# Keep the upper bound. Cookie 2.0.0 removed the `parse` and `serialize`
|
||||
# exports, and SvelteKit needs them. Cookie 1.x corrects the vulnerability and
|
||||
# keeps both names.
|
||||
'cookie@<0.7.0': '>=0.7.0 <2.0.0'
|
||||
'tough-cookie@<4.1.3': '>=4.1.3'
|
||||
'nanoid@<3.3.8': '>=3.3.8'
|
||||
'form-data@<2.5.4': '>=2.5.4'
|
||||
'glob@>=10.2.0 <10.5.0': '>=10.5.0'
|
||||
'esbuild@<=0.24.2': '>=0.25.0'
|
||||
'@eslint/plugin-kit@<0.3.4': '>=0.3.4'
|
||||
|
||||
peerDependencyRules:
|
||||
allowedVersions:
|
||||
# vite-plugin-tailwind-purgecss 0.3.5 is the most recent release, and its
|
||||
# last change was December 2024. It declares support only through Vite 6,
|
||||
# but it does its work correctly with Vite 8: the plugin decreases the CSS
|
||||
# output from 208 KB to 72 KB. Accept the newer Vite to hide a warning that
|
||||
# gives no information. Remove this rule if the plugin gets a new release,
|
||||
# or when a move to Tailwind 4 makes the plugin unnecessary.
|
||||
'vite-plugin-tailwind-purgecss>vite': '8'
|
||||
|
||||
# "allowBuilds" replaces the "onlyBuiltDependencies" list. Each package that
|
||||
# runs install scripts needs an entry with the value true. pnpm blocks the
|
||||
# scripts of all other packages.
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
svelte-preprocess: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
221
web/src/app.css
221
web/src/app.css
|
|
@ -1,134 +1,111 @@
|
|||
/* @tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* Tailwind 4 reads its configuration from CSS. The settings in this file come
|
||||
* from the tailwind.config.ts file that it replaces.
|
||||
*
|
||||
* Every @import must come before the other rules. Tailwind needs to read its
|
||||
* own theme before it reads the Skeleton CSS, because Skeleton uses the
|
||||
* Tailwind breakpoint variants. */
|
||||
@import 'tailwindcss';
|
||||
|
||||
:root {
|
||||
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Go Mono', 'Fira Sans', 'Helvetica Neue', sans-serif;
|
||||
/* Skeleton 5 supplies CSS only. The Svelte components are in a separate
|
||||
* package. */
|
||||
@import '@skeletonlabs/skeleton';
|
||||
|
||||
/* Each theme that theme-store.ts can select needs an import. The v2 names
|
||||
* "skeleton" and "gold-nouveau" are now "legacy" and "nouveau". */
|
||||
@import '@skeletonlabs/skeleton/themes/legacy';
|
||||
@import '@skeletonlabs/skeleton/themes/modern';
|
||||
@import '@skeletonlabs/skeleton/themes/crimson';
|
||||
@import '@skeletonlabs/skeleton/themes/nouveau';
|
||||
@import '@skeletonlabs/skeleton/themes/hamlindigo';
|
||||
@import '@skeletonlabs/skeleton/themes/vintage';
|
||||
@import '@skeletonlabs/skeleton/themes/seafoam';
|
||||
@import '@skeletonlabs/skeleton/themes/sahara';
|
||||
@import '@skeletonlabs/skeleton/themes/rocket';
|
||||
@import './themes/my-custom-theme.css';
|
||||
|
||||
@plugin '@tailwindcss/forms';
|
||||
@plugin '@tailwindcss/typography';
|
||||
|
||||
/* Tailwind must scan the Skeleton component package to keep the classes that
|
||||
* those components use. */
|
||||
@source '../node_modules/@skeletonlabs/skeleton-svelte/dist';
|
||||
|
||||
/* The v3 config set darkMode to "class". Tailwind 4 needs this variant to read
|
||||
* the "dark" class that app.html puts on the html element. */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--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;
|
||||
} */
|
||||
/* These colors read CSS variables that no file defines, because the block
|
||||
* that declared them in app.css is commented out. Tailwind 3 behaved the
|
||||
* same way, so the declarations stay as they are to hold the appearance
|
||||
* steady. Define the variables in this file to make the colors work. */
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
/* 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%;
|
||||
--animate-blink: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
@keyframes blink {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
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;
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
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;
|
||||
/* Tailwind 4 no longer takes container settings from a configuration file. */
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-muted-foreground/30 rounded-full hover:bg-muted-foreground/50 transition-colors;
|
||||
} */
|
||||
html,
|
||||
body {
|
||||
@apply h-full overflow-hidden;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
@apply font-mono text-sm;
|
||||
}
|
||||
|
||||
.terminal-input {
|
||||
@apply font-mono text-sm;
|
||||
}
|
||||
|
||||
/* The typography plugin puts quotation marks around inline code. Remove them. */
|
||||
.prose :where(code):not(:where([class~='not-prose'] *))::before,
|
||||
.prose :where(code):not(:where([class~='not-prose'] *))::after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
/* Theme overrides. The v2 "skeleton" theme is now "legacy". */
|
||||
:root [data-theme='legacy'] {
|
||||
--typo-base--font-family:
|
||||
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
--typo-heading--font-family:
|
||||
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
@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;
|
||||
}
|
||||
|
|
@ -46,12 +46,14 @@ export const api = {
|
|||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error('Response body is null');
|
||||
|
||||
// Decode in streaming mode: a multi-byte UTF-8 rune split across network
|
||||
// chunks is otherwise decoded as two halves and corrupted into U+FFFD.
|
||||
const decoder = new TextDecoder();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
yield decoder.decode(value);
|
||||
|
||||
if (done) break;
|
||||
|
||||
yield decoder.decode(value, { stream: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
56
web/src/lib/api/models.test.ts
Normal file
56
web/src/lib/api/models.test.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { api } from './base';
|
||||
import { modelsApi } from './models';
|
||||
|
||||
describe('modelsApi.getAvailable', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns one entry for each model of each vendor', async () => {
|
||||
vi.spyOn(api, 'fetch').mockResolvedValue({
|
||||
data: { models: ['a', 'b'], vendors: { Anthropic: ['a'], OpenAI: ['b'] } }
|
||||
});
|
||||
|
||||
const models = await modelsApi.getAvailable();
|
||||
|
||||
expect(models).toEqual([
|
||||
{ name: 'a', vendor: 'Anthropic' },
|
||||
{ name: 'b', vendor: 'OpenAI' }
|
||||
]);
|
||||
});
|
||||
|
||||
// The server sends null for the model list of a vendor that it can reach no
|
||||
// models for, because an empty slice in Go becomes null in JSON. Ollama does
|
||||
// this when it is in the configuration but has no models. One such vendor
|
||||
// must not stop the models of the other vendors.
|
||||
it('skips a vendor whose model list is null', async () => {
|
||||
vi.spyOn(api, 'fetch').mockResolvedValue({
|
||||
data: {
|
||||
models: ['a'],
|
||||
vendors: { Anthropic: ['a'], Ollama: null } as unknown as Record<string, string[]>
|
||||
}
|
||||
});
|
||||
|
||||
const models = await modelsApi.getAvailable();
|
||||
|
||||
expect(models).toEqual([{ name: 'a', vendor: 'Anthropic' }]);
|
||||
});
|
||||
|
||||
it('skips a vendor whose model list is not an array', async () => {
|
||||
vi.spyOn(api, 'fetch').mockResolvedValue({
|
||||
data: {
|
||||
models: [],
|
||||
vendors: { Broken: 'not-a-list' } as unknown as Record<string, string[]>
|
||||
}
|
||||
});
|
||||
|
||||
await expect(modelsApi.getAvailable()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('reports an error when the response holds no vendors', async () => {
|
||||
vi.spyOn(api, 'fetch').mockResolvedValue({ data: { models: [] } as never });
|
||||
|
||||
await expect(modelsApi.getAvailable()).rejects.toThrow('missing vendors data');
|
||||
});
|
||||
});
|
||||
|
|
@ -10,11 +10,17 @@ export const modelsApi = {
|
|||
throw new Error('Invalid response format: missing vendors data');
|
||||
}
|
||||
|
||||
// The server sends null for the model list of a vendor that it can find
|
||||
// no models for, because an empty slice in Go becomes null in JSON.
|
||||
// Ollama does this when it is in the configuration but serves no models.
|
||||
// Skip such a vendor: one of them must not hide the models of the others.
|
||||
return Object.entries(response.data.vendors).flatMap(([vendor, models]) =>
|
||||
models.map(model => ({
|
||||
name: model,
|
||||
vendor
|
||||
}))
|
||||
Array.isArray(models)
|
||||
? models.map(model => ({
|
||||
name: model,
|
||||
vendor
|
||||
}))
|
||||
: []
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch models:", error);
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@
|
|||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { obsidianSettings } from "$lib/store/obsidian-store";
|
||||
import { featureFlags } from "$lib/config/features";
|
||||
import { getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
import { drawerStore } from '$lib/store/drawer-store';
|
||||
import { systemPrompt, selectedPatternName } from "$lib/store/pattern-store";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
function openDrawer() {
|
||||
drawerStore.open({});
|
||||
}
|
||||
|
|
@ -156,7 +155,7 @@
|
|||
$: showObsidian = $featureFlags.enableObsidianIntegration;
|
||||
</script>
|
||||
|
||||
<div class="chat-container flex gap-0 p-2 w-full h-screen">
|
||||
<div class="chat-container flex h-full min-h-0 w-full gap-0 p-2">
|
||||
<!-- Left Column -->
|
||||
<aside class="flex flex-col gap-2 pr-2 left-column" style="width: {leftColumnWidth}%">
|
||||
<!-- Dropdowns Group with Model Config -->
|
||||
|
|
|
|||
11
web/src/lib/components/chat/Chat.test.ts
Normal file
11
web/src/lib/components/chat/Chat.test.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./Chat.svelte', import.meta.url), 'utf8');
|
||||
|
||||
describe('Chat viewport layout', () => {
|
||||
it('fits the route viewport instead of extending to the full screen height', () => {
|
||||
expect(source).toMatch(/class="chat-container[^"]*\bh-full\b[^"]*\bmin-h-0\b/);
|
||||
expect(source).not.toMatch(/class="chat-container[^"]*\bh-screen\b/);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { sendMessage, messageStore } from '$lib/store/chat-store';
|
||||
import { messageStore } from '$lib/store/chat-store';
|
||||
import { systemPrompt, selectedPatternName } from '$lib/store/pattern-store';
|
||||
import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import { FileButton } from '@skeletonlabs/skeleton';
|
||||
import { toastStore } from '$lib/store/toast-store';
|
||||
import { Paperclip, Send, FileCheck } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
|
@ -14,6 +13,8 @@
|
|||
import { languageStore } from '$lib/store/language-store';
|
||||
import { obsidianSettings, updateObsidianSettings } from '$lib/store/obsidian-store';
|
||||
import { PdfConversionService } from '$lib/services/PdfConversionService';
|
||||
import { formatErrorMessage } from '$lib/utils/error-message';
|
||||
import { readFileContent } from './read-file-content';
|
||||
|
||||
const pdfService = new PdfConversionService();
|
||||
|
||||
|
|
@ -23,7 +24,6 @@
|
|||
const chatService = new ChatService();
|
||||
let userInput = "";
|
||||
let isYouTubeURL = false;
|
||||
const toastStore = getToastStore();
|
||||
let files: FileList | undefined = undefined;
|
||||
let uploadedFiles: string[] = [];
|
||||
let fileContents: string[] = [];
|
||||
|
|
@ -84,13 +84,11 @@
|
|||
|
||||
async function handleFileUpload(e: Event) {
|
||||
uploadedFiles = []; // Clear uploadedFiles at the beginning
|
||||
isFileIndicatorVisible = false;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
if (uploadedFiles.length >= 5 || (uploadedFiles.length + files.length) > 5) {
|
||||
toastStore.trigger({
|
||||
message: 'Maximum 5 files allowed',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
toastStore.error('Maximum 5 files allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -105,9 +103,10 @@
|
|||
|
||||
for (let i = 0; i < files.length && uploadedFiles.length < 5; i++) {
|
||||
const file = files[i];
|
||||
const content = await readFileContent(file);
|
||||
const content = await readFileContent(file, pdfService, toastStore.warning);
|
||||
fileContents.push(content);
|
||||
uploadedFiles = [...uploadedFiles, file.name];
|
||||
isFileIndicatorVisible = true;
|
||||
|
||||
// Update processing status per file
|
||||
messageStore.update(messages => {
|
||||
|
|
@ -126,10 +125,7 @@
|
|||
);
|
||||
|
||||
} catch (error) {
|
||||
toastStore.trigger({
|
||||
message: 'Error processing files: ' + (error as Error).message,
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
toastStore.error('Error processing files: ' + (error as Error).message);
|
||||
|
||||
// Clean up processing message on error
|
||||
messageStore.update(messages =>
|
||||
|
|
@ -141,104 +137,6 @@
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function readFileContent(file: File): Promise<string> {
|
||||
// Log initial file metadata
|
||||
console.log('Reading file:', {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: new Date(file.lastModified).toISOString()
|
||||
});
|
||||
|
||||
// Handle PDF files
|
||||
if (file.type === 'application/pdf') {
|
||||
try {
|
||||
// Start PDF processing
|
||||
console.log('Starting PDF conversion process');
|
||||
const markdown = await pdfService.convertToMarkdown(file);
|
||||
|
||||
// Validate conversion result
|
||||
console.log('PDF conversion completed:', {
|
||||
resultLength: markdown.length,
|
||||
preview: markdown.substring(0, 100)
|
||||
});
|
||||
|
||||
// Ensure we have valid content
|
||||
if (!markdown || markdown.trim().length === 0) {
|
||||
throw new Error('PDF conversion returned empty content');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Add to fileContents for pattern processing
|
||||
fileContents.push(markdown);
|
||||
|
||||
// Prepare enhanced prompt with system instructions
|
||||
const enhancedPrompt = `${$systemPrompt}\nAnalyze and process the provided content according to these instructions.`;
|
||||
|
||||
// Format final content with proper labeling
|
||||
const finalContent = `${userInput}\n\nFile Contents (PDF):\n${markdown}`;
|
||||
|
||||
// Process through pattern system
|
||||
await sendMessage(finalContent, enhancedPrompt);
|
||||
|
||||
return markdown;
|
||||
|
||||
} catch (error) {
|
||||
console.error('PDF Conversion error:', {
|
||||
error,
|
||||
fileName: file.name,
|
||||
fileSize: file.size
|
||||
});
|
||||
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error during PDF conversion';
|
||||
|
||||
throw new Error(`Failed to convert PDF ${file.name}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle text files
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = async (e) => {
|
||||
const content = e.target?.result as string;
|
||||
console.log('Text file processed:', {
|
||||
fileName: file.name,
|
||||
contentLength: content.length,
|
||||
preview: content.substring(0, 100)
|
||||
});
|
||||
// resolve(content);
|
||||
const enhancedPrompt = `${$systemPrompt}\nAnalyze and process the provided content according to these instructions.`;
|
||||
const finalContent = `${userInput}\n\nFile Contents (Text):\n${content}`;
|
||||
await sendMessage(finalContent, enhancedPrompt);
|
||||
resolve(content);
|
||||
};
|
||||
|
||||
reader.onerror = (e) => {
|
||||
console.error('FileReader error:', {
|
||||
error: reader.error,
|
||||
fileName: file.name
|
||||
});
|
||||
reject(new Error(`Failed to read ${file.name}: ${reader.error?.message}`));
|
||||
};
|
||||
|
||||
// Start reading the file
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function saveToObsidian(content: string) {
|
||||
if (!$obsidianSettings.saveToObsidian) {
|
||||
console.log('Obsidian saving is disabled');
|
||||
|
|
@ -246,26 +144,17 @@ async function readFileContent(file: File): Promise<string> {
|
|||
}
|
||||
|
||||
if (!$obsidianSettings.noteName) {
|
||||
toastStore.trigger({
|
||||
message: 'Please enter a note name in Obsidian settings',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
toastStore.error('Please enter a note name in Obsidian settings');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$selectedPatternName) {
|
||||
toastStore.trigger({
|
||||
message: 'No pattern selected',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
toastStore.error('No pattern selected');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
toastStore.trigger({
|
||||
message: 'No content to save',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
toastStore.error('No content to save');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -292,16 +181,10 @@ async function readFileContent(file: File): Promise<string> {
|
|||
saveToObsidian: false, // Reset the save flag
|
||||
noteName: '' // Clear the note name
|
||||
});
|
||||
toastStore.trigger({
|
||||
message: responseData.message || `Saved to Obsidian: ${responseData.fileName}`,
|
||||
background: 'variant-filled-success'
|
||||
});
|
||||
toastStore.success(responseData.message || `Saved to Obsidian: ${responseData.fileName}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to save to Obsidian:', error);
|
||||
toastStore.trigger({
|
||||
message: error instanceof Error ? error.message : 'Failed to save to Obsidian',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
toastStore.error(error instanceof Error ? error.message : 'Failed to save to Obsidian');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -354,6 +237,7 @@ async function readFileContent(file: File): Promise<string> {
|
|||
const contentsForProcessing = [...fileContents];
|
||||
uploadedFiles = [];
|
||||
fileContents = [];
|
||||
isFileIndicatorVisible = false;
|
||||
fileButtonKey = !fileButtonKey;
|
||||
|
||||
// If the message contains YouTube URLs, replace them with transcripts
|
||||
|
|
@ -412,17 +296,22 @@ async function readFileContent(file: File): Promise<string> {
|
|||
|
||||
(error) => {
|
||||
// Make sure to remove loading message on error
|
||||
messageStore.update(messages =>
|
||||
messageStore.update(messages =>
|
||||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
console.error('Stream processing error:', error);
|
||||
|
||||
// Show error message using a valid format type
|
||||
|
||||
const message = formatErrorMessage(error);
|
||||
|
||||
// Show the error in the chat, where it stays for the person to read.
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'system',
|
||||
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
content: message,
|
||||
format: 'plain'
|
||||
}]);
|
||||
// And as a toast, so that a failure is visible even when the chat is
|
||||
// scrolled away from the end.
|
||||
toastStore.error(message);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
|
|
@ -440,12 +329,15 @@ async function readFileContent(file: File): Promise<string> {
|
|||
messages.filter(m => m.format !== 'loading')
|
||||
);
|
||||
|
||||
// Show error message using a valid format type
|
||||
const message = formatErrorMessage(error);
|
||||
|
||||
// Show the error in the chat and as a toast, as the stream handler does.
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'system',
|
||||
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
content: message,
|
||||
format: 'plain'
|
||||
}]);
|
||||
toastStore.error(message);
|
||||
} finally {
|
||||
// As a final safety measure, ensure loading message is removed
|
||||
messageStore.update(messages =>
|
||||
|
|
@ -454,54 +346,6 @@ async function readFileContent(file: File): Promise<string> {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/* async function handleSubmit() {
|
||||
if (!userInput.trim()) return;
|
||||
|
||||
try {
|
||||
console.log('\n=== Submit Handler Start ===');
|
||||
|
||||
if (isYouTubeURL) {
|
||||
console.log('2a. Starting YouTube flow');
|
||||
await processYouTubeURL(userInput);
|
||||
return;
|
||||
}
|
||||
|
||||
const enhancedPrompt = fileContents.length > 0
|
||||
? `${$systemPrompt}\nAnalyze and process the provided content according to these instructions.`
|
||||
: $systemPrompt;
|
||||
|
||||
// Hide raw content from display but keep it for processing
|
||||
messageStore.update(messages => [...messages, {
|
||||
role: 'system',
|
||||
content: 'Processing content...',
|
||||
format: 'loading'
|
||||
}]);
|
||||
|
||||
// Store the user input before clearing it
|
||||
const inputText = userInput;
|
||||
|
||||
// Construct finalContent BEFORE clearing userInput
|
||||
const finalContent = fileContents.length > 0
|
||||
? `${inputText}\n\nFile Contents (${uploadedFiles.map(f => f.endsWith('.pdf') ? 'PDF' : 'Text').join(', ')}):\n${fileContents.join('\n\n---\n\n')}`
|
||||
: inputText;
|
||||
|
||||
// Now clear the input fields
|
||||
userInput = "";
|
||||
uploadedFiles = [];
|
||||
fileContents = [];
|
||||
fileButtonKey = !fileButtonKey;
|
||||
|
||||
await sendMessage(finalContent, enhancedPrompt);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Chat submission error:', error);
|
||||
}
|
||||
} */
|
||||
|
||||
|
||||
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
|
@ -531,17 +375,25 @@ async function readFileContent(file: File): Promise<string> {
|
|||
</span>
|
||||
{/if}
|
||||
{#key fileButtonKey}
|
||||
<FileButton
|
||||
name="file-upload"
|
||||
button="btn-icon variant-ghost"
|
||||
bind:files
|
||||
on:change={handleFileUpload}
|
||||
disabled={isProcessingFiles || uploadedFiles.length >= 5}
|
||||
class="h-10 w-10 bg-primary-800/30 hover:bg-primary-800/50 rounded-full transition-colors"
|
||||
<!-- Skeleton 5 replaced FileButton with FileUpload, which draws a drop
|
||||
zone and reports files through a callback. A label with a hidden file
|
||||
input keeps both the appearance and the change handler of the button
|
||||
that was here before. -->
|
||||
<label
|
||||
class="btn-icon preset-tonal inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-primary-800/30 transition-colors hover:bg-primary-800/50"
|
||||
class:pointer-events-none={isProcessingFiles || uploadedFiles.length >= 5}
|
||||
class:opacity-50={isProcessingFiles || uploadedFiles.length >= 5}
|
||||
>
|
||||
<Paperclip class="w-5 h-5" />
|
||||
|
||||
</FileButton>
|
||||
<input
|
||||
type="file"
|
||||
name="file-upload"
|
||||
class="hidden"
|
||||
bind:files
|
||||
on:change={handleFileUpload}
|
||||
disabled={isProcessingFiles || uploadedFiles.length >= 5}
|
||||
/>
|
||||
<Paperclip class="w-5 h-5" />
|
||||
</label>
|
||||
{/key}
|
||||
<Button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ function renderContent(message: Message): string {
|
|||
|
||||
|
||||
<div class="message-header flex items-center gap-2 mb-1 {message.role === 'assistant' || message.role === 'system' ? '' : 'justify-end'}">
|
||||
<span class="text-xs text-muted-foreground rounded-lg p-1 variant-glass-secondary font-bold uppercase">
|
||||
<span class="text-xs text-muted-foreground rounded-lg p-1 bg-secondary-500/20 backdrop-blur-lg font-bold uppercase">
|
||||
{#if message.role === 'system'}
|
||||
SYSTEM
|
||||
{:else if message.role === 'assistant'}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<script lang='ts'>
|
||||
import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import { toastStore } from '$lib/store/toast-store';
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Input from '$lib/components/ui/input/Input.svelte';
|
||||
import { Toast } from '@skeletonlabs/skeleton';
|
||||
|
||||
let url = '';
|
||||
let transcript = '';
|
||||
|
|
@ -10,7 +9,6 @@
|
|||
let error = '';
|
||||
let title = '';
|
||||
|
||||
const toastStore = getToastStore();
|
||||
|
||||
async function fetchTranscript() {
|
||||
function isValidYouTubeUrl(url: string) {
|
||||
|
|
@ -20,10 +18,7 @@
|
|||
|
||||
if (!isValidYouTubeUrl(url)) {
|
||||
error = 'Please enter a valid YouTube URL';
|
||||
toastStore.trigger({
|
||||
message: error,
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
toastStore.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -59,15 +54,9 @@
|
|||
async function copyToClipboard() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(transcript);
|
||||
toastStore.trigger({
|
||||
message: 'Transcript copied to clipboard!',
|
||||
background: 'variant-filled-success'
|
||||
});
|
||||
toastStore.success('Transcript copied to clipboard!');
|
||||
} catch (err) {
|
||||
toastStore.trigger({
|
||||
message: 'Failed to copy transcript',
|
||||
background: 'variant-filled-error'
|
||||
});
|
||||
toastStore.error('Failed to copy transcript');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -98,7 +87,6 @@
|
|||
{/if}
|
||||
|
||||
{#if transcript}
|
||||
<Toast position="l" />
|
||||
<div class="space-y-4 border rounded-lg p-4 bg-muted/50 h-96">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-xs font-semibold">{title || 'Transcript'}</h3>
|
||||
|
|
|
|||
65
web/src/lib/components/chat/read-file-content.test.ts
Normal file
65
web/src/lib/components/chat/read-file-content.test.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { readFileContent } from './read-file-content';
|
||||
|
||||
const pdfBytes = new Uint8Array([0x25, 0x50, 0x44, 0x46]);
|
||||
|
||||
describe('readFileContent', () => {
|
||||
const noPdf = {
|
||||
convertToMarkdown: () => Promise.reject(new Error('unexpected PDF conversion'))
|
||||
};
|
||||
|
||||
it('resolves with the text of a text file', async () => {
|
||||
const file = new File(['# Notes\n\nPlain text.\n'], 'notes.md', { type: 'text/markdown' });
|
||||
|
||||
await expect(readFileContent(file, noPdf, () => {})).resolves.toBe('# Notes\n\nPlain text.\n');
|
||||
});
|
||||
|
||||
it('resolves with the converted Markdown of a PDF file', async () => {
|
||||
const pdf = {
|
||||
convertToMarkdown: vi.fn().mockResolvedValue({ markdown: '# Title\n' })
|
||||
};
|
||||
const file = new File([pdfBytes], 'report.pdf', { type: 'application/pdf' });
|
||||
|
||||
await expect(readFileContent(file, pdf, () => {})).resolves.toBe('# Title\n');
|
||||
expect(pdf.convertToMarkdown).toHaveBeenCalledWith(file);
|
||||
});
|
||||
|
||||
it('forwards the conversion warning of a PDF file', async () => {
|
||||
const pdf = {
|
||||
convertToMarkdown: () => Promise.resolve({ markdown: '# Title\n', warning: 'partial text' })
|
||||
};
|
||||
const warn = vi.fn();
|
||||
const file = new File([pdfBytes], 'report.pdf', { type: 'application/pdf' });
|
||||
|
||||
await readFileContent(file, pdf, warn);
|
||||
|
||||
expect(warn).toHaveBeenCalledWith('partial text');
|
||||
});
|
||||
});
|
||||
|
||||
// The repository has no browser test runner, so these tests pin the chat
|
||||
// boundary at the source level: the component has no sendMessage path, and
|
||||
// handleSubmit holds the one streamChat call. Together with the seam tests
|
||||
// above, this proves that attachment parses without a chat request and that
|
||||
// one submit sends one request.
|
||||
describe('ChatInput chat boundary', () => {
|
||||
const source = readFileSync(new URL('./ChatInput.svelte', import.meta.url), 'utf8');
|
||||
|
||||
it('has no sendMessage path, so a file attachment cannot send a chat request', () => {
|
||||
expect(source.includes('sendMessage')).toBe(false);
|
||||
});
|
||||
|
||||
it('sends through exactly one streamChat call, in handleSubmit', () => {
|
||||
expect(source.match(/\.streamChat\(/g) ?? []).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows the attachment indicator after processing and hides it after submit', () => {
|
||||
expect(source).toMatch(
|
||||
/uploadedFiles = \[\.\.\.uploadedFiles, file\.name\];\s*isFileIndicatorVisible = true;/
|
||||
);
|
||||
expect(source).toMatch(
|
||||
/uploadedFiles = \[\];\s*fileContents = \[\];\s*isFileIndicatorVisible = false;/
|
||||
);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue