Compare commits

...

89 commits

Author SHA1 Message Date
github-actions[bot] b682dad740 chore(release): Update version to v1.4.478
Some checks failed
Update Version File and Create Tag / update-version (push) Has been cancelled
Go Build / Run tests (push) Has been cancelled
2026-09-07 19:17:43 +00:00
Kayvan Sylvan e91b4f0af1
Merge pull request #2216 from ctbaum/fix/gpt-6-raw-mode
fix: enable raw mode for GPT-6 models
2026-09-07 12:15:38 -07:00
ctbaum f8bc07f2d7 chore: incoming 2216 changelog entry 2026-09-06 12:57:58 +02:00
ctbaum 8db08ad20f fix: enable raw mode for GPT-6 models 2026-09-06 12:57:33 +02:00
github-actions[bot] 6b0914d1bb chore(release): Update version to v1.4.477
Some checks failed
Go Build / Run tests (push) Has been cancelled
Update Version File and Create Tag / update-version (push) Has been cancelled
2026-09-03 22:45:25 +00:00
Kayvan Sylvan 3ca2133819
Merge pull request #2211 from ksylvan/fix-left-behind-tempdir
fix: prevent pattern loader temporary directory leaks
2026-09-03 15:42:28 -07:00
Kayvan Sylvan f141b68f12 fix: prevent pattern loader temporary directory leaks
- Create pattern temporary directories only during database population.
- Remove temporary directories after successful or failed downloads.
- Add regression tests for lazy creation and cleanup.
- Document pattern loader cleanup behavior in changelog.
2026-09-03 15:28:35 -07:00
github-actions[bot] 8dce453ed7 chore(release): Update version to v1.4.476 2026-09-03 22:05:51 +00:00
Kayvan Sylvan 4637fa1a22
Merge pull request #2210 from ksylvan/add-pzero-to-vendors
feat: add Pzero as an OpenAI-compatible AI provider
2026-09-03 15:03:29 -07:00
Kayvan Sylvan a3d1dc2cae feat: add Pzero as an OpenAI-compatible AI provider
- Register Pzero with its OpenAI-compatible API endpoint.
- Disable unsupported Responses API behavior for Pzero requests.
- List Pzero among supported providers in README documentation.
- Add Pzero integration details to incoming changelog.
2026-09-03 15:00:39 -07:00
github-actions[bot] 00f56a76e0 chore(release): Update version to v1.4.475 2026-09-03 16:48:09 +00:00
Kayvan Sylvan e9b32ba546
Merge pull request #2209 from kadiryildiz283/feat/add-turkish-locale
feat(i18n): add Turkish (tr) translation
2026-09-03 09:45:59 -07:00
Kayvan Sylvan 4835506879 chore: incoming 2209 changelog entry 2026-09-03 09:43:13 -07:00
Kayvan Sylvan 852ffd8468 chore: add newline to turkish locale json file 2026-09-03 09:43:08 -07:00
kadiryildiz283 fc0b2c0294 feat(i18n): add Turkish (tr) translation 2026-09-03 09:09:57 +03:00
github-actions[bot] a8afe60608 chore(release): Update version to v1.4.474
Some checks are pending
Go Build / Run tests (push) Waiting to run
Update Version File and Create Tag / update-version (push) Waiting to run
2026-09-03 00:58:10 +00:00
Kayvan Sylvan 892c70a8bb
Merge pull request #2206 from ksylvan/fix/storage-path-traversal
fix: confine storage names and authenticate Ollama serve
2026-09-02 17:55:55 -07:00
Kayvan Sylvan 63846b523c chore: incoming 2206 changelog entry 2026-09-02 16:00:22 -07:00
Kayvan Sylvan f37c71f9f7 fix: secure storage paths and authenticate REST and Ollama servers
- Reject unsafe cross-platform storage names and traversal attempts.
- Confine symlink targets within configured filesystem storage directories.
- Require API keys for every non-loopback server binding.
- Authenticate Ollama routes and securely forward configured credentials.
- Validate chat pattern, context, and session names early.
- Sanitize client errors to hide internal filesystem details.
- Default REST server binding to loopback port 8080.
- Add regression coverage for traversal, symlinks, and authentication.
2026-09-02 15:59:59 -07:00
github-actions[bot] cc2a9d39d6 chore(release): Update version to v1.4.473
Some checks failed
Go Build / Run tests (push) Has been cancelled
Update Version File and Create Tag / update-version (push) Has been cancelled
2026-08-29 16:37:15 +00:00
Kayvan Sylvan 95c5b7d6ba
Merge pull request #2203 from pacocartones/fix/sse-utf8-stream-boundary
fix(web): preserve multi-byte UTF-8 split across SSE chunks in chat stream
2026-08-29 09:34:56 -07:00
Kayvan Sylvan b136259cfb chore: simplify incoming changelog snippet 2026-08-29 09:30:10 -07:00
Kayvan Sylvan 52f63d0795 chore: incoming 2203 changelog entry 2026-08-29 09:27:15 -07:00
Kayvan Sylvan 2c510971dd 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
2026-08-29 09:26:35 -07:00
Kayvan Sylvan b185a5c790 Merge branch 'main' into fix/sse-utf8-stream-boundary 2026-08-29 08:59:06 -07:00
github-actions[bot] 8fd6ce2418 chore(release): Update version to v1.4.472 2026-08-29 15:48:52 +00:00
Kayvan Sylvan 8aeaa1bb5e
Merge pull request #2198 from cuihuan/add-synthorai-provider
feat(providers): add Synthorai as an OpenAI-compatible provider
2026-08-29 08:46:36 -07:00
Kayvan Sylvan 3ad89a3516 chore: simplify incoming changelog snippet 2026-08-29 08:43:51 -07:00
Kayvan Sylvan cd54c1a82f Merge branch 'main' into add-synthorai-provider 2026-08-29 08:42:12 -07:00
Kayvan Sylvan 435b89d936 chore: incoming 2198 changelog entry 2026-08-29 08:41:47 -07:00
Paco Cartones c10bd5a380 fix(web): preserve multi-byte UTF-8 split across SSE chunks in chat stream
createMessageStream decoded each network chunk with a fresh TextDecoder and no
{ stream: true }, so a multi-byte UTF-8 rune (CJK, emoji) split across a chunk
boundary was corrupted into U+FFFD before the frame buffer ever saw it. Hoist a
single persistent decoder and decode with { stream: true } so pending bytes
carry across chunks.
2026-08-29 00:17:59 +00:00
Kayvan Sylvan 374d2dc967 chore: delete generate_changelog noise in README.
Some checks failed
Go Build / Run tests (push) Waiting to run
Update Version File and Create Tag / update-version (push) Waiting to run
Patterns Artifact / Zip and Upload Patterns Folder (push) Has been cancelled
2026-08-28 07:49:20 -07:00
github-actions[bot] 54a7775e5b chore(release): Update version to v1.4.471 2026-08-28 02:55:44 +00:00
Kayvan Sylvan 19b87b33a2
Merge pull request #2200 from ksylvan/fix/codex-oauth-token-persist
fix: persist Codex OAuth tokens after refresh
2026-08-27 19:53:41 -07:00
Kayvan Sylvan 1b907fe318 chore: incoming 2200 changelog entry 2026-08-27 18:46:25 -07:00
Kayvan Sylvan 9b724f8cec fix: persist refreshed Codex tokens and reactivate configured vendors
- Persist rotated Codex OAuth tokens after every successful refresh.
- Reload stored credentials under cross-process locks before refreshing.
- Write environment updates atomically with secret-only file permissions.
- Preserve existing environment values while skipping empty replacements.
- Activate configured vendors lazily and surface configuration failures immediately.
- Reuse valid refresh tokens before starting interactive authentication.
- Localize Codex setup, provider, request, and persistence errors.
- Upgrade AI, cloud, telemetry, and supporting Go dependencies.
- Set Claude Opus 5 as default changelog summarizer.
- Document `.env` token sensitivity and rewrite behavior.
2026-08-27 18:12:08 -07:00
cuihuan 733a1810bf feat(providers): add Synthorai as an OpenAI-compatible provider
Synthorai (https://synthorai.io) reaches Anthropic, OpenAI, Google, Z.ai,
Moonshot, DeepSeek and Qwen models through one OpenAI-compatible base URL,
billed at each upstream's own list price.
2026-08-26 11:26:51 +08:00
github-actions[bot] 338b89cfe9 chore(release): Update version to v1.4.470 2026-08-04 01:20:12 +00:00
Kayvan Sylvan c9211929c4
Merge pull request #2186 from giodamelio/update-nixpkgs
Update Nixpkgs version for newer Go version
2026-08-03 18:17:55 -07:00
Kayvan Sylvan 9614bf2f16 chore: incoming 2186 changelog entry 2026-08-03 18:11:08 -07:00
Giovanni d'Amelio 2580d130c0 chore: update Nixpkgs version for newer Go version
The go.mod now requires go >= 1.26.0 and the current Nixpkgs has 1.25.4
2026-08-03 13:10:08 -07:00
github-actions[bot] 0cb6a9facc chore(release): Update version to v1.4.469 2026-08-03 17:58:51 +00:00
Kayvan Sylvan 6627337e54
Merge pull request #2185 from ksylvan/issue-2184-pdf-inspector-wasm
feat(web): replace PDF.js pipeline with pdf-inspector WASM worker
2026-08-03 10:56:37 -07:00
Kayvan Sylvan 56d330f5e8 fix(web): keep chat history within viewport 2026-08-03 10:52:29 -07:00
Kayvan Sylvan 829fa3bd07 fix(web): show processed file attachment indicator 2026-08-03 06:44:07 -07:00
Kayvan Sylvan 28273a0533 build: upgrade SvelteKit, Shiki, and Vite web dependencies
- Upgrade SvelteKit to 2.70.2 for framework maintenance updates
- Upgrade Shiki to 4.4.1 across syntax highlighting packages
- Upgrade Vite to 8.2.0 and Rolldown to 1.2.1
- Synchronize npm and pnpm lockfiles with transitive dependencies
2026-08-02 17:07:01 -07:00
Kayvan Sylvan ab64555925 Merge branch 'main' into issue-2184-pdf-inspector-wasm 2026-08-02 16:36:24 -07:00
github-actions[bot] cb340b6b02 chore(release): Update version to v1.4.468 2026-08-02 23:32:11 +00:00
Kayvan Sylvan 32ff0b5591
Merge pull request #2182 from drawliin/fix/ollama-stream-channel-close 2026-08-02 16:29:55 -07:00
Ssam 2b1b31ca3f
Merge branch 'main' into fix/ollama-stream-channel-close 2026-08-03 00:28:08 +01:00
Kayvan Sylvan b90a07bc19 feat(web): replace PDF.js conversion with WASM worker
- Process PDF attachments through reusable pdf-inspector WASM worker
- Transfer ArrayBuffers and initialize WASM once per worker
- Reject pending conversions and recreate workers after crashes
- Parse attachments without triggering duplicate chat requests
- Surface OCR, encoding, and empty-content conversion errors
- Remove PDF.js pipeline and obsolete transitive dependencies
- Add tests for conversion results and chat boundaries
2026-08-02 16:25:25 -07:00
Kayvan Sylvan a4b89f65c2 chore: incoming 2182 changelog entry 2026-08-02 16:24:27 -07:00
github-actions[bot] 9e92bca148 chore(release): Update version to v1.4.467 2026-07-31 15:49:11 +00:00
Kayvan Sylvan fe2fbe9cff
Merge pull request #2171 from OdinKral/pr/pattern-audit-script
feat(scripts): add pattern/maintenance audit script
2026-07-31 08:46:47 -07:00
Kayvan Sylvan c9fa92bf03
Merge branch 'main' into pr/pattern-audit-script 2026-07-31 08:41:19 -07:00
Kayvan Sylvan 7a638ff3cc chore: incoming 2171 changelog entry 2026-07-31 07:30:11 -07:00
github-actions[bot] bd4c9c93d2 chore(release): Update version to v1.4.466 2026-07-30 23:07:25 +00:00
Kayvan Sylvan b12a1ef1eb
Merge pull request #2116 from danielmiessler/dependabot/npm_and_yarn/web/npm_and_yarn-9ebba0a229
fix(web): repair the chat page and modernize the build toolchain
2026-07-30 16:05:03 -07:00
Kayvan Sylvan 8135a11606 docs: correct PR 2116 contributor attribution
- Credit ksylvan and Dependabot for PR 2116 changes
2026-07-30 16:01:24 -07:00
Kayvan Sylvan fba2543895 chore: incoming 2116 changelog entry 2026-07-30 15:57:32 -07:00
Kayvan Sylvan e8fd77c40b style(web): format the files added in this branch
Prettier reflows these three files, which the branch adds or rewrites. The count
of files that prettier reports goes from 333 to 330. The rest of that count is
work that this branch does not touch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:50:41 -07:00
Kayvan Sylvan 9f38ee0eff chore: incoming 2116 changelog entry 2026-07-30 15:21:17 -07:00
Kayvan Sylvan 1b1feea2b5 feat: modernize web stack and harden chat error handling
- Upgrade Tailwind, Skeleton, SvelteKit, Vite, and supporting dependencies.
- Replace removed Skeleton components with compatible local implementations.
- Report pre-stream chat failures without duplicating streamed errors.
- Return empty model arrays and skip malformed vendor lists.
- Prevent stream completion crashes and normalize displayed error messages.
- Stack toasts correctly and add warning notification support.
- Restore linting, formatting, testing, and pnpm override configuration.
- Preserve custom themes through Tailwind CSS-based configuration migration.
2026-07-30 15:06:58 -07:00
Kayvan Sylvan 737c3ac372
Merge pull request #2178 from OdinKral/feat/generate-frontmatter-pattern
feat(patterns): add generate_frontmatter for PKM/Obsidian users
2026-07-30 12:41:50 -07:00
Kayvan Sylvan e25005c8d5 feat: register generate_frontmatter for pattern discovery workflows
- Add PKM YAML metadata guidance to pattern explanations
- Classify frontmatter generation under conversion, extraction, and writing
- Register descriptions and extracts for pattern suggestion workflows
- Credit both contributors in the incoming changelog entry
2026-07-30 12:37:21 -07:00
Kayvan Sylvan 9e685112af chore: incoming 2178 changelog entry 2026-07-30 12:28:19 -07:00
drawliin e65c35d63e fix(ollama): close stream channel on errors 2026-07-30 02:28:31 +01:00
github-actions[bot] d8e934df22 chore(release): Update version to v1.4.465 2026-07-29 22:41:15 +00:00
Kayvan Sylvan 7f2e663cb9
Merge pull request #2180 from ksylvan/fix/grok-web-search-help
fix: include Grok in localized --search help text
2026-07-29 15:39:04 -07:00
Kayvan Sylvan 01f88e5c2b chore: incoming 2180 changelog entry 2026-07-29 15:28:13 -07:00
Kayvan Sylvan 9730808119 fix: include Grok in localized --search help text
PR #2092 added Grok (xAI) web search support and updated the go-flags
struct tag in internal/cli/flags.go, but `fabric --help` renders flag
descriptions from the i18n message catalog (internal/cli/help.go maps
"search" -> "enable_web_search_tool"), not from the struct tag. The
catalog value still listed only Anthropic, OpenAI, and Gemini, so the
Grok support stayed undiscoverable in --help even though it works.

Append "Grok" to the enable_web_search_tool value in all 11 locales,
using each locale's own list separator, and update the generated README
help block to match. No functional change.

The zsh and fish completion strings were already corrected in #2179,
so this carries forward only the remaining parts of #2140.

Co-Authored-By: Ken Hartman <khartman@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 15:22:22 -07:00
github-actions[bot] 6cef8c10ad chore(release): Update version to v1.4.464 2026-07-29 22:15:14 +00:00
Kayvan Sylvan cd8c249744
Merge pull request #2179 from ksylvan/fix-completion-scripts
fix: declare completion arguments and synchronize CLI help
2026-07-29 15:12:51 -07:00
Kayvan Sylvan 080311effc docs: clarify show-metadata token details across CLI surfaces
- Specify input and output tokens in flag help text
- Synchronize metadata descriptions across shell completion definitions
- Localize clarified metadata guidance across supported languages
- Record description clarification in incoming changelog entry
2026-07-29 15:11:05 -07:00
Kayvan Sylvan f95bff568f chore: incoming 2179 changelog entry 2026-07-29 15:01:41 -07:00
Kayvan Sylvan ec0b7246c4 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
- Localize pattern, Spotify, and metadata help descriptions consistently
- Document expanded completion coverage and wire-level debug behavior
- Describe YouTube visual flags directly in generated help
2026-07-29 14:56:27 -07:00
OdinKral 3b03c17f9a feat(scripts): add pattern/maintenance audit script
Adds scripts/audit-patterns.sh, a dependency-free report over the
patterns library and a few adjacent consistency checks:

  1. Thin patterns     — system.md under 15 lines (likely stubs)
  2. Bloated patterns  — system.md over 50 KB (likely embedded examples)
  3. Stale model refs  — hardcoded GPT-4 / ChatGPT / vendor model names
  4. Missing INPUT     — pattern has no `# INPUT` section
  5. i18n key gaps     — keys in en.json missing from other locales
  6. Completion gaps   — long flags in flags.go absent from completions

Reports only; it never edits patterns. Exits 0 by default so it can be
run casually, or `--strict` to exit 1 on findings for CI use.

Against the current tree it reports 109 issues, which is what motivated
the separate normalization and de-bloating changes.
2026-07-29 08:22:34 -04:00
github-actions[bot] 4ba8ac2919 chore(release): Update version to v1.4.463 2026-07-28 17:10:26 +00:00
Kayvan Sylvan 3e7c4b902a
Merge pull request #2168 from ksylvan/i18n-audit
fix: complete localized setup and error messages for supported locales
2026-07-28 10:08:09 -07:00
Kayvan Sylvan 177363a702 chore: incoming 2168 changelog entry 2026-07-28 10:04:43 -07:00
Kayvan Sylvan 3ff434ed2b fix: complete localized setup and error messages across supported locales
- Translate Bedrock setup prompts across nine supported locales
- Localize datetime and system template errors consistently
- Translate Persian Spotify errors and setup guidance
- Correct Japanese and Polish file operation log labels
2026-07-28 09:42:40 -07:00
github-actions[bot] ba6feced62 chore(release): Update version to v1.4.462 2026-07-28 15:30:08 +00:00
Kayvan Sylvan 69dbf44ad1
Merge pull request #2167 from ksylvan/fix/pattern-path-traversal-2123-re-created
fix: block path traversal in pattern name lookup (closes #2094)
2026-07-28 08:27:46 -07:00
Kayvan Sylvan 64b1fc1fab chore: renumber incoming changelog entry 2123 -> 2167 2026-07-28 08:24:41 -07:00
Kayvan Sylvan 37fc6686fa docs: credit security fix contributors and note translations
- Credit both contributors for the path traversal security fix.
- Document new translations for the invalid pattern message.
2026-07-28 08:15:07 -07:00
Kayvan Sylvan e36a231f3f chore: incoming 2123 changelog entry 2026-07-28 08:11:17 -07:00
Kayvan Sylvan 54347b5cf5 fix(i18n): translate pattern_invalid_name into all 10 non-English locales 2026-07-28 08:08:03 -07:00
OdinKral 1e650266ab fix: block path traversal in pattern name lookup (closes #2094)
Pattern names containing ".." could be used to escape the patterns
directory and read arbitrary files via filepath.Join. Guard added at
the top of getFromDB; i18n key pattern_invalid_name added to all
11 locale files; test cases cover all common traversal variants.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 08:08:03 -07:00
OdinKral 175772e8a0 feat(patterns): add generate_frontmatter pattern for PKM users
Adds a pattern that generates clean, paste-ready YAML frontmatter for
any text input. Designed for PKM systems such as Obsidian and Logseq
but works for any markdown-based note vault.

Given a document, article, transcript, or rough draft it produces:
  title, aliases, tags (specific + hierarchical), type, author,
  source, date, summary, and status fields.

Usage:
  cat article.md | fabric -p generate_frontmatter
  pbpaste       | fabric -p generate_frontmatter
  yt URL        | fabric -p generate_frontmatter

Addresses feature request #1235.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:41:20 -04:00
135 changed files with 11402 additions and 5177 deletions

View file

@ -1,5 +1,149 @@
# 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 READMEs 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
- Fix: complete localized setup and error messages across supported locales
- Translate Bedrock setup prompts across nine supported locales
- Localize datetime and system template errors consistently
- Translate Persian Spotify errors and setup guidance
- Correct Japanese and Polish file operation log labels
## v1.4.462 (2026-07-28)
### PR [#2123](https://github.com/danielmiessler/Fabric/pull/2123) by [ksylvan](https://github.com/ksylvan) and [OdinKral](https://github.com/OdinKral): fix: block path traversal in pattern name lookup
- **Security Fix:** Blocked path traversal attacks in pattern name lookup (closes #2094) — pattern names containing `..` could previously escape the patterns directory and read arbitrary files via `filepath.Join`; a guard has been added at the top of `getFromDB`, an i18n key `pattern_invalid_name` has been added to all 11 locale files, and test cases now cover all common traversal variants.
- New translations for the "invalid pattern" user-facing string.
## v1.4.461 (2026-07-28)
### PR [#2152](https://github.com/danielmiessler/Fabric/pull/2152) by [AUTHENSOR](https://github.com/AUTHENSOR): fix: shell-escape extension values to prevent command injection

View file

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

View file

@ -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 级别(完整的请求和响应内容)
### 演习模式

View file

@ -1,3 +1,3 @@
package main
var version = "v1.4.461"
var version = "v1.4.478"

View file

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

View file

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

View file

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

View file

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

View file

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

View 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 38 specific, lowercase tags that describe the content. Prefer concrete concepts over vague categories. Avoid single-word generic tags like "notes" or "text".
- Generate 13 aliases: alternative titles or short names someone might search for.
- Write a one-sentence summary (1525 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:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -54,7 +54,7 @@
"bedrock_aws_access_key_label": "Geben Sie Ihre AWS Access Key ID ein (leer lassen, um die AWS-Anmeldekette zu verwenden)",
"bedrock_aws_region_label": "AWS-Region",
"bedrock_aws_secret_key_label": "Geben Sie Ihren AWS Secret Access Key ein (leer lassen, um die AWS-Anmeldekette zu verwenden)",
"bedrock_client_not_initialized": "Bedrock client not initialized — run 'fabric --setup' to configure",
"bedrock_client_not_initialized": "Bedrock-Client nicht initialisiert — führen Sie 'fabric --setup' zur Konfiguration aus",
"bedrock_converse_failed": "Bedrock Converse für Modell %s fehlgeschlagen: %w",
"bedrock_conversestream_failed": "Bedrock ConverseStream für Modell %s fehlgeschlagen: %w",
"bedrock_empty_response_content": "leerer Antwortinhalt",
@ -67,21 +67,21 @@
"bedrock_listmodels_fallback": "Bedrock ListModels API fehlgeschlagen, verwende statische Fallback-Liste",
"bedrock_panic_sendstream": "Panic in SendStream: %v",
"bedrock_setup_auth_option_accesskey": " [2] AWS Access Key + Secret Key",
"bedrock_setup_auth_option_apikey": " [1] Bedrock API Key / ABSK token (recommended — same as Claude Code)",
"bedrock_setup_auth_prompt": "Enter 1 or 2",
"bedrock_setup_choose_auth_method": " Choose authentication method:",
"bedrock_setup_choose_model": " Choose default model (unprefixed IDs work in any region; us./eu./ap. are region-specific):",
"bedrock_setup_choose_region": " Choose AWS Region:",
"bedrock_setup_auth_option_apikey": " [1] Bedrock API-Schlüssel / ABSK-Token (empfohlen — derselbe wie bei Claude Code)",
"bedrock_setup_auth_prompt": "Geben Sie 1 oder 2 ein",
"bedrock_setup_choose_auth_method": " Authentifizierungsmethode wählen:",
"bedrock_setup_choose_model": " Standardmodell wählen (IDs ohne Präfix funktionieren in jeder Region; us./eu./ap. sind regionsspezifisch):",
"bedrock_setup_choose_region": " AWS-Region wählen:",
"bedrock_setup_header": "[Bedrock]",
"bedrock_setup_invalid_auth_selection": "invalid selection: %s (enter 1 or 2)",
"bedrock_setup_model_custom_prompt": "Enter model ID (e.g. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Enter a different model ID",
"bedrock_setup_model_prompt": "Enter model number or 0 to type your own",
"bedrock_setup_region_custom_prompt": "Enter custom AWS region (e.g. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Enter a different region",
"bedrock_setup_region_prompt": "Enter region number or 0 for custom",
"bedrock_setup_selected_model": " ✓ Selected model: %s",
"bedrock_setup_use_with": " Use with: fabric -m %s -V Bedrock",
"bedrock_setup_invalid_auth_selection": "Ungültige Auswahl: %s (geben Sie 1 oder 2 ein)",
"bedrock_setup_model_custom_prompt": "Geben Sie die Modell-ID ein (z. B. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Eine andere Modell-ID eingeben",
"bedrock_setup_model_prompt": "Geben Sie die Modellnummer ein oder 0, um eine eigene einzugeben",
"bedrock_setup_region_custom_prompt": "Geben Sie eine eigene AWS-Region ein (z. B. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Eine andere Region eingeben",
"bedrock_setup_region_prompt": "Geben Sie die Regionsnummer ein oder 0 für eine eigene",
"bedrock_setup_selected_model": " ✓ Ausgewähltes Modell: %s",
"bedrock_setup_use_with": " Verwendung: fabric -m %s -V Bedrock",
"bedrock_unable_load_aws_config": "AWS-Konfiguration konnte nicht geladen werden: %w",
"bedrock_unable_load_aws_config_with_region": "AWS-Konfiguration konnte mit Region %s nicht geladen werden: %w",
"bedrock_unexpected_content_block_type": "unerwarteter Inhaltsblocktyp: %T",
@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "Video-Metadaten ausgeben",
"path_to_yaml_config": "Pfad zur YAML-Konfigurationsdatei",
"pattern_not_found_list_available": "Pattern '%s' nicht gefunden. Führen Sie 'fabric -l' aus, um verfügbare Patterns anzuzeigen",
"pattern_invalid_name": "Ungültiger Pattern-Name: %q",
"pattern_not_found_no_patterns": "Pattern '%s' nicht gefunden.\n\nKeine Patterns installiert! Um dies zu beheben:\n • Führen Sie 'fabric --setup' aus, um Patterns zu konfigurieren und herunterzuladen\n • Oder führen Sie 'fabric -U' aus, um Patterns direkt herunterzuladen/zu aktualisieren",
"pattern_variables_help": "Werte für Mustervariablen, z.B. -v=#role:expert -v=#points:30",
"patterns_cloning_repository": "Repository %s wird geklont (Pfad: %s)...\\n",
@ -491,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",
@ -504,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",
@ -599,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",
@ -610,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",
@ -628,11 +643,11 @@
"strategy_path_traversal": "Strategiename %q löst sich außerhalb des Strategieverzeichnisses auf",
"stream_help": "Streaming",
"suppress_thinking_tags": "In Denk-Tags eingeschlossenen Text unterdrücken",
"template_datetime_error_invalid_number": "invalid number in relative time: %q",
"template_datetime_error_invalid_relative_format": "invalid relative time format",
"template_datetime_error_invalid_unit": "invalid time unit: %q",
"template_datetime_error_relative_requires_value": "relative time requires a value",
"template_datetime_error_unknown_operation": "datetime: unknown operation %q",
"template_datetime_error_invalid_number": "ungültige Zahl in relativer Zeitangabe: %q",
"template_datetime_error_invalid_relative_format": "ungültiges Format für relative Zeitangabe",
"template_datetime_error_invalid_unit": "ungültige Zeiteinheit: %q",
"template_datetime_error_relative_requires_value": "relative Zeitangabe erfordert einen Wert",
"template_datetime_error_unknown_operation": "datetime: unbekannte Operation %q",
"template_extension_error": "Erweiterung %s Fehler: %v",
"template_file_error_expand_home_dir": "datei: home-verzeichnis konnte nicht erweitert werden: %v",
"template_file_error_invalid_line_count": "datei: ungültige zeilenanzahl %q",
@ -661,12 +676,12 @@
"template_missing_required_variable": "Erforderliche Variable fehlt: %s",
"template_plugin_error": "Plugin %s Fehler: %v",
"template_processing_stuck": "Vorlagenverarbeitung blockiert - mögliche Endlosschleife",
"template_sys_error_env_requires_var": "env: variable name required",
"template_sys_error_home": "failed to get home directory: %v",
"template_sys_error_hostname": "failed to get hostname: %v",
"template_sys_error_pwd": "failed to get working directory: %v",
"template_sys_error_unknown_operation": "sys: unknown operation %q",
"template_sys_error_user": "failed to get current user: %v",
"template_sys_error_env_requires_var": "env: Variablenname erforderlich",
"template_sys_error_home": "Home-Verzeichnis konnte nicht ermittelt werden: %v",
"template_sys_error_hostname": "Hostname konnte nicht ermittelt werden: %v",
"template_sys_error_pwd": "Arbeitsverzeichnis konnte nicht ermittelt werden: %v",
"template_sys_error_unknown_operation": "sys: unbekannte Operation %q",
"template_sys_error_user": "aktueller Benutzer konnte nicht ermittelt werden: %v",
"template_text_empty_input": "Text: Leere Eingabe für Operation %q",
"template_text_unknown_operation": "Text: Unbekannte Textoperation %q (unterstützt: upper, lower, title, trim)",
"template_unknown_plugin_namespace": "Unbekannter Plugin-Namespace: %s",

View file

@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "Output video metadata",
"path_to_yaml_config": "Path to YAML config file",
"pattern_not_found_list_available": "pattern '%s' not found. Run 'fabric -l' to see available patterns",
"pattern_invalid_name": "invalid pattern name: %q",
"pattern_not_found_no_patterns": "pattern '%s' not found.\n\nNo patterns are installed! To fix this:\n • Run 'fabric --setup' to configure and download patterns\n • Or run 'fabric -U' to download/update patterns directly",
"pattern_variables_help": "Values for pattern variables, e.g. -v=#role:expert -v=#points:30",
"patterns_cloning_repository": "Cloning repository %s (path: %s)...\n",
@ -491,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",
@ -504,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",
@ -599,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",
@ -610,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",

View file

@ -54,7 +54,7 @@
"bedrock_aws_access_key_label": "Ingrese su AWS Access Key ID (deje vacío para usar la cadena de credenciales de AWS)",
"bedrock_aws_region_label": "Región de AWS",
"bedrock_aws_secret_key_label": "Ingrese su AWS Secret Access Key (deje vacío para usar la cadena de credenciales de AWS)",
"bedrock_client_not_initialized": "Bedrock client not initialized — run 'fabric --setup' to configure",
"bedrock_client_not_initialized": "cliente de Bedrock no inicializado — ejecute 'fabric --setup' para configurar",
"bedrock_converse_failed": "bedrock converse falló para el modelo %s: %w",
"bedrock_conversestream_failed": "bedrock conversestream falló para el modelo %s: %w",
"bedrock_empty_response_content": "contenido de respuesta vacío",
@ -67,21 +67,21 @@
"bedrock_listmodels_fallback": "la API ListModels de Bedrock falló, usando lista estática de respaldo",
"bedrock_panic_sendstream": "pánico en SendStream: %v",
"bedrock_setup_auth_option_accesskey": " [2] AWS Access Key + Secret Key",
"bedrock_setup_auth_option_apikey": " [1] Bedrock API Key / ABSK token (recommended — same as Claude Code)",
"bedrock_setup_auth_prompt": "Enter 1 or 2",
"bedrock_setup_choose_auth_method": " Choose authentication method:",
"bedrock_setup_choose_model": " Choose default model (unprefixed IDs work in any region; us./eu./ap. are region-specific):",
"bedrock_setup_choose_region": " Choose AWS Region:",
"bedrock_setup_auth_option_apikey": " [1] Clave API de Bedrock / token ABSK (recomendado — la misma que usa Claude Code)",
"bedrock_setup_auth_prompt": "Introduzca 1 o 2",
"bedrock_setup_choose_auth_method": " Elija el método de autenticación:",
"bedrock_setup_choose_model": " Elija el modelo predeterminado (los ID sin prefijo funcionan en cualquier región; us./eu./ap. son específicos de región):",
"bedrock_setup_choose_region": " Elija la región de AWS:",
"bedrock_setup_header": "[Bedrock]",
"bedrock_setup_invalid_auth_selection": "invalid selection: %s (enter 1 or 2)",
"bedrock_setup_model_custom_prompt": "Enter model ID (e.g. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Enter a different model ID",
"bedrock_setup_model_prompt": "Enter model number or 0 to type your own",
"bedrock_setup_region_custom_prompt": "Enter custom AWS region (e.g. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Enter a different region",
"bedrock_setup_region_prompt": "Enter region number or 0 for custom",
"bedrock_setup_selected_model": " ✓ Selected model: %s",
"bedrock_setup_use_with": " Use with: fabric -m %s -V Bedrock",
"bedrock_setup_invalid_auth_selection": "selección inválida: %s (introduzca 1 o 2)",
"bedrock_setup_model_custom_prompt": "Introduzca el ID del modelo (p. ej. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Introducir un ID de modelo diferente",
"bedrock_setup_model_prompt": "Introduzca el número del modelo o 0 para escribir el suyo",
"bedrock_setup_region_custom_prompt": "Introduzca una región de AWS personalizada (p. ej. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Introducir una región diferente",
"bedrock_setup_region_prompt": "Introduzca el número de la región o 0 para personalizarla",
"bedrock_setup_selected_model": " ✓ Modelo seleccionado: %s",
"bedrock_setup_use_with": " Uso: fabric -m %s -V Bedrock",
"bedrock_unable_load_aws_config": "no se pudo cargar la configuración de AWS: %w",
"bedrock_unable_load_aws_config_with_region": "no se pudo cargar la configuración de AWS con región %s: %w",
"bedrock_unexpected_content_block_type": "tipo de bloque de contenido inesperado: %T",
@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "Salida de metadatos del video",
"path_to_yaml_config": "Ruta al archivo de configuración YAML",
"pattern_not_found_list_available": "patrón '%s' no encontrado. Ejecuta 'fabric -l' para ver los patrones disponibles",
"pattern_invalid_name": "nombre de patrón inválido: %q",
"pattern_not_found_no_patterns": "patrón '%s' no encontrado.\n\n¡No hay patrones instalados! Para solucionar esto:\n • Ejecuta 'fabric --setup' para configurar y descargar patrones\n • O ejecuta 'fabric -U' para descargar/actualizar patrones directamente",
"pattern_variables_help": "Valores para variables de patrón, ej. -v=#role:expert -v=#points:30",
"patterns_cloning_repository": "Clonando el repositorio %s (ruta: %s)...\\n",
@ -491,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",
@ -504,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",
@ -599,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",
@ -610,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",
@ -628,11 +643,11 @@
"strategy_path_traversal": "el nombre de estrategia %q se resuelve fuera del directorio de estrategias",
"stream_help": "Transmitir",
"suppress_thinking_tags": "Suprimir texto encerrado en etiquetas de pensamiento",
"template_datetime_error_invalid_number": "invalid number in relative time: %q",
"template_datetime_error_invalid_relative_format": "invalid relative time format",
"template_datetime_error_invalid_unit": "invalid time unit: %q",
"template_datetime_error_relative_requires_value": "relative time requires a value",
"template_datetime_error_unknown_operation": "datetime: unknown operation %q",
"template_datetime_error_invalid_number": "número inválido en el tiempo relativo: %q",
"template_datetime_error_invalid_relative_format": "formato de tiempo relativo inválido",
"template_datetime_error_invalid_unit": "unidad de tiempo inválida: %q",
"template_datetime_error_relative_requires_value": "el tiempo relativo requiere un valor",
"template_datetime_error_unknown_operation": "datetime: operación desconocida %q",
"template_extension_error": "Error en extensión %s: %v",
"template_file_error_expand_home_dir": "archivo: no se pudo expandir el directorio home: %v",
"template_file_error_invalid_line_count": "archivo: número de líneas no válido %q",
@ -661,12 +676,12 @@
"template_missing_required_variable": "Variable requerida faltante: %s",
"template_plugin_error": "Error en plugin %s: %v",
"template_processing_stuck": "Procesamiento de plantilla bloqueado - posible bucle infinito",
"template_sys_error_env_requires_var": "env: variable name required",
"template_sys_error_home": "failed to get home directory: %v",
"template_sys_error_hostname": "failed to get hostname: %v",
"template_sys_error_pwd": "failed to get working directory: %v",
"template_sys_error_unknown_operation": "sys: unknown operation %q",
"template_sys_error_user": "failed to get current user: %v",
"template_sys_error_env_requires_var": "env: se requiere el nombre de la variable",
"template_sys_error_home": "no se pudo obtener el directorio de inicio: %v",
"template_sys_error_hostname": "no se pudo obtener el nombre del host: %v",
"template_sys_error_pwd": "no se pudo obtener el directorio de trabajo: %v",
"template_sys_error_unknown_operation": "sys: operación desconocida %q",
"template_sys_error_user": "no se pudo obtener el usuario actual: %v",
"template_text_empty_input": "Texto: entrada vacía para la operación %q",
"template_text_unknown_operation": "Texto: operación de texto desconocida %q (soportadas: upper, lower, title, trim)",
"template_unknown_plugin_namespace": "Espacio de nombres de plugin desconocido: %s",

View file

@ -54,7 +54,7 @@
"bedrock_aws_access_key_label": "AWS Access Key ID خود را وارد کنید (برای استفاده از زنجیره اعتبارنامه AWS خالی بگذارید)",
"bedrock_aws_region_label": "منطقه AWS",
"bedrock_aws_secret_key_label": "AWS Secret Access Key خود را وارد کنید (برای استفاده از زنجیره اعتبارنامه AWS خالی بگذارید)",
"bedrock_client_not_initialized": "Bedrock client not initialized — run 'fabric --setup' to configure",
"bedrock_client_not_initialized": "کلاینت Bedrock مقداردهی نشده است — 'fabric --setup' را برای پیکربندی اجرا کنید",
"bedrock_converse_failed": "bedrock converse برای مدل %s ناموفق بود: %w",
"bedrock_conversestream_failed": "bedrock conversestream برای مدل %s ناموفق بود: %w",
"bedrock_empty_response_content": "محتوای پاسخ خالی",
@ -67,21 +67,21 @@
"bedrock_listmodels_fallback": "API ListModels Bedrock ناموفق بود، استفاده از لیست پشتیبان ثابت",
"bedrock_panic_sendstream": "پنیک در SendStream: %v",
"bedrock_setup_auth_option_accesskey": " [2] AWS Access Key + Secret Key",
"bedrock_setup_auth_option_apikey": " [1] Bedrock API Key / ABSK token (recommended — same as Claude Code)",
"bedrock_setup_auth_prompt": "Enter 1 or 2",
"bedrock_setup_choose_auth_method": " Choose authentication method:",
"bedrock_setup_choose_model": " Choose default model (unprefixed IDs work in any region; us./eu./ap. are region-specific):",
"bedrock_setup_choose_region": " Choose AWS Region:",
"bedrock_setup_auth_option_apikey": " [1] کلید API Bedrock / توکن ABSK (توصیه‌شده — همان کلید Claude Code)",
"bedrock_setup_auth_prompt": "1 یا 2 را وارد کنید",
"bedrock_setup_choose_auth_method": " روش احراز هویت را انتخاب کنید:",
"bedrock_setup_choose_model": " مدل پیش‌فرض را انتخاب کنید (شناسه‌های بدون پیشوند در هر منطقه‌ای کار می‌کنند؛ us./eu./ap. مخصوص منطقه هستند):",
"bedrock_setup_choose_region": " منطقه AWS را انتخاب کنید:",
"bedrock_setup_header": "[Bedrock]",
"bedrock_setup_invalid_auth_selection": "invalid selection: %s (enter 1 or 2)",
"bedrock_setup_model_custom_prompt": "Enter model ID (e.g. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Enter a different model ID",
"bedrock_setup_model_prompt": "Enter model number or 0 to type your own",
"bedrock_setup_region_custom_prompt": "Enter custom AWS region (e.g. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Enter a different region",
"bedrock_setup_region_prompt": "Enter region number or 0 for custom",
"bedrock_setup_selected_model": " ✓ Selected model: %s",
"bedrock_setup_use_with": " Use with: fabric -m %s -V Bedrock",
"bedrock_setup_invalid_auth_selection": "انتخاب نامعتبر: %s (1 یا 2 را وارد کنید)",
"bedrock_setup_model_custom_prompt": "شناسه مدل را وارد کنید (مثلاً anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] وارد کردن شناسه مدل دیگر",
"bedrock_setup_model_prompt": "شماره مدل را وارد کنید یا برای وارد کردن شناسه دلخواه 0 را بزنید",
"bedrock_setup_region_custom_prompt": "منطقه AWS دلخواه را وارد کنید (مثلاً us-east-1)",
"bedrock_setup_region_option_custom": " [0] وارد کردن منطقه دیگر",
"bedrock_setup_region_prompt": "شماره منطقه را وارد کنید یا برای منطقه دلخواه 0 را بزنید",
"bedrock_setup_selected_model": " ✓ مدل انتخاب‌شده: %s",
"bedrock_setup_use_with": " نحوه استفاده: fabric -m %s -V Bedrock",
"bedrock_unable_load_aws_config": "بارگذاری پیکربندی AWS ناموفق بود: %w",
"bedrock_unable_load_aws_config_with_region": "بارگذاری پیکربندی AWS با منطقه %s ناموفق بود: %w",
"bedrock_unexpected_content_block_type": "نوع بلوک محتوای غیرمنتظره: %T",
@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "نمایش فراداده ویدیو",
"path_to_yaml_config": "مسیر فایل پیکربندی YAML",
"pattern_not_found_list_available": "الگوی '%s' یافت نشد. برای مشاهده الگوهای موجود 'fabric -l' را اجرا کنید",
"pattern_invalid_name": "نام الگوی نامعتبر: %q",
"pattern_not_found_no_patterns": "الگوی '%s' یافت نشد.\n\nهیچ الگویی نصب نشده است! برای رفع این مشکل:\n • 'fabric --setup' را برای پیکربندی و دانلود الگوها اجرا کنید\n • یا 'fabric -U' را برای دانلود/به‌روزرسانی الگوها اجرا کنید",
"pattern_variables_help": "مقادیر برای متغیرهای الگو، مثال: -v=#role:expert -v=#points:30",
"patterns_cloning_repository": "در حال کلون کردن مخزن %s (مسیر: %s)...\\n",
@ -491,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": "حذف افزونه ثبت شده با نام",
@ -504,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": "تنظیم جریمه فرکانس",
@ -564,7 +577,7 @@
"spotify_description_header": "## توضیحات",
"spotify_duration_label": "**مدت زمان**: %d دقیقه",
"spotify_episode_header": "# اپیزود اسپاتیفای",
"spotify_error_getting_metadata": "error getting Spotify metadata: %v",
"spotify_error_getting_metadata": "خطا در دریافت فراداده Spotify: %v",
"spotify_explicit_label": "**محتوای صریح**: %v",
"spotify_failed_create_request": "ایجاد درخواست ناموفق بود: %w",
"spotify_failed_create_token_request": "ایجاد درخواست توکن ناموفق بود: %w",
@ -578,14 +591,14 @@
"spotify_failed_parse_show_metadata": "تجزیه فراداده برنامه ناموفق بود: %w",
"spotify_failed_read_response_body": "خواندن متن پاسخ ناموفق بود: %w",
"spotify_failed_request_access_token": "درخواست توکن دسترسی ناموفق بود: %w",
"spotify_invalid_url": "invalid Spotify URL, can't get show or episode ID: '%s'",
"spotify_invalid_url": "نشانی Spotify نامعتبر است، دریافت شناسه برنامه یا قسمت ممکن نیست: '%s'",
"spotify_label": "Spotify",
"spotify_language_field_label": "**زبان**: %s",
"spotify_languages_label": "**زبان‌ها**: %s",
"spotify_media_type_label": "**نوع رسانه**: %s",
"spotify_no_episode_found": "no episode found with ID: %s",
"spotify_no_show_found": "no show found with ID: %s",
"spotify_not_configured": "Spotify is not configured, please run the setup procedure",
"spotify_no_episode_found": "هیچ قسمتی با این شناسه یافت نشد: %s",
"spotify_no_show_found": "هیچ برنامه‌ای با این شناسه یافت نشد: %s",
"spotify_not_configured": "Spotify پیکربندی نشده است، لطفاً روند راه‌اندازی را اجرا کنید",
"spotify_publisher_label": "**ناشر**: %s",
"spotify_release_date_label": "**تاریخ انتشار**: %s",
"spotify_search_description_label": "- **توضیحات**: %s",
@ -594,11 +607,12 @@
"spotify_search_publisher_label": "- **ناشر**: %s",
"spotify_search_results_header": "# نتایج جستجوی اسپاتیفای",
"spotify_search_url_label": "- **URL**: %s",
"spotify_setup_description": "Spotify - to grab podcast/show metadata from Spotify",
"spotify_setup_description": "Spotify - برای دریافت فراداده پادکست/برنامه از Spotify",
"spotify_show_header": "# پادکست/برنامه اسپاتیفای",
"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",
@ -610,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",
@ -628,11 +643,11 @@
"strategy_path_traversal": "نام راهبرد %q خارج از دایرکتوری راهبردها حل می‌شود",
"stream_help": "پخش زنده",
"suppress_thinking_tags": "سرکوب متن محصور در تگ‌های تفکر",
"template_datetime_error_invalid_number": "invalid number in relative time: %q",
"template_datetime_error_invalid_relative_format": "invalid relative time format",
"template_datetime_error_invalid_unit": "invalid time unit: %q",
"template_datetime_error_relative_requires_value": "relative time requires a value",
"template_datetime_error_unknown_operation": "datetime: unknown operation %q",
"template_datetime_error_invalid_number": "عدد نامعتبر در زمان نسبی: %q",
"template_datetime_error_invalid_relative_format": "قالب زمان نسبی نامعتبر است",
"template_datetime_error_invalid_unit": "واحد زمانی نامعتبر: %q",
"template_datetime_error_relative_requires_value": "زمان نسبی به یک مقدار نیاز دارد",
"template_datetime_error_unknown_operation": "datetime: عملیات ناشناخته %q",
"template_extension_error": "خطای افزونه %s: %v",
"template_file_error_expand_home_dir": "فایل: گسترش پوشه خانگی ممکن نشد: %v",
"template_file_error_invalid_line_count": "فایل: تعداد خط نامعتبر %q",
@ -661,12 +676,12 @@
"template_missing_required_variable": "متغیر الزامی موجود نیست: %s",
"template_plugin_error": "خطای پلاگین %s: %v",
"template_processing_stuck": "پردازش قالب متوقف شده - احتمال حلقه بی‌نهایت",
"template_sys_error_env_requires_var": "env: variable name required",
"template_sys_error_home": "failed to get home directory: %v",
"template_sys_error_hostname": "failed to get hostname: %v",
"template_sys_error_pwd": "failed to get working directory: %v",
"template_sys_error_unknown_operation": "sys: unknown operation %q",
"template_sys_error_user": "failed to get current user: %v",
"template_sys_error_env_requires_var": "env: نام متغیر لازم است",
"template_sys_error_home": "دریافت پوشه خانگی ناموفق بود: %v",
"template_sys_error_hostname": "دریافت نام میزبان ناموفق بود: %v",
"template_sys_error_pwd": "دریافت پوشه کاری ناموفق بود: %v",
"template_sys_error_unknown_operation": "sys: عملیات ناشناخته %q",
"template_sys_error_user": "دریافت کاربر فعلی ناموفق بود: %v",
"template_text_empty_input": "متن: ورودی خالی برای عملیات %q",
"template_text_unknown_operation": "متن: عملیات متنی ناشناخته %q (پشتیبانی شده: upper, lower, title, trim)",
"template_unknown_plugin_namespace": "فضای نام پلاگین ناشناخته: %s",

View file

@ -54,7 +54,7 @@
"bedrock_aws_access_key_label": "Entrez votre AWS Access Key ID (laissez vide pour utiliser la chaîne d'authentification AWS)",
"bedrock_aws_region_label": "Région AWS",
"bedrock_aws_secret_key_label": "Entrez votre AWS Secret Access Key (laissez vide pour utiliser la chaîne d'authentification AWS)",
"bedrock_client_not_initialized": "Bedrock client not initialized — run 'fabric --setup' to configure",
"bedrock_client_not_initialized": "client Bedrock non initialisé — exécutez 'fabric --setup' pour configurer",
"bedrock_converse_failed": "bedrock converse a échoué pour le modèle %s : %w",
"bedrock_conversestream_failed": "bedrock conversestream a échoué pour le modèle %s : %w",
"bedrock_empty_response_content": "contenu de réponse vide",
@ -67,21 +67,21 @@
"bedrock_listmodels_fallback": "API ListModels Bedrock échouée, utilisation de la liste statique de secours",
"bedrock_panic_sendstream": "panique dans SendStream : %v",
"bedrock_setup_auth_option_accesskey": " [2] AWS Access Key + Secret Key",
"bedrock_setup_auth_option_apikey": " [1] Bedrock API Key / ABSK token (recommended — same as Claude Code)",
"bedrock_setup_auth_prompt": "Enter 1 or 2",
"bedrock_setup_choose_auth_method": " Choose authentication method:",
"bedrock_setup_choose_model": " Choose default model (unprefixed IDs work in any region; us./eu./ap. are region-specific):",
"bedrock_setup_choose_region": " Choose AWS Region:",
"bedrock_setup_auth_option_apikey": " [1] Clé API Bedrock / jeton ABSK (recommandé — identique à Claude Code)",
"bedrock_setup_auth_prompt": "Entrez 1 ou 2",
"bedrock_setup_choose_auth_method": " Choisissez la méthode d'authentification :",
"bedrock_setup_choose_model": " Choisissez le modèle par défaut (les ID sans préfixe fonctionnent dans toutes les régions ; us./eu./ap. sont spécifiques à une région) :",
"bedrock_setup_choose_region": " Choisissez la région AWS :",
"bedrock_setup_header": "[Bedrock]",
"bedrock_setup_invalid_auth_selection": "invalid selection: %s (enter 1 or 2)",
"bedrock_setup_model_custom_prompt": "Enter model ID (e.g. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Enter a different model ID",
"bedrock_setup_model_prompt": "Enter model number or 0 to type your own",
"bedrock_setup_region_custom_prompt": "Enter custom AWS region (e.g. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Enter a different region",
"bedrock_setup_region_prompt": "Enter region number or 0 for custom",
"bedrock_setup_selected_model": " ✓ Selected model: %s",
"bedrock_setup_use_with": " Use with: fabric -m %s -V Bedrock",
"bedrock_setup_invalid_auth_selection": "sélection invalide : %s (entrez 1 ou 2)",
"bedrock_setup_model_custom_prompt": "Entrez l'ID du modèle (par ex. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Entrer un autre ID de modèle",
"bedrock_setup_model_prompt": "Entrez le numéro du modèle ou 0 pour saisir le vôtre",
"bedrock_setup_region_custom_prompt": "Entrez une région AWS personnalisée (par ex. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Entrer une autre région",
"bedrock_setup_region_prompt": "Entrez le numéro de la région ou 0 pour une région personnalisée",
"bedrock_setup_selected_model": " ✓ Modèle sélectionné : %s",
"bedrock_setup_use_with": " Utilisation : fabric -m %s -V Bedrock",
"bedrock_unable_load_aws_config": "impossible de charger la configuration AWS : %w",
"bedrock_unable_load_aws_config_with_region": "impossible de charger la configuration AWS avec la région %s : %w",
"bedrock_unexpected_content_block_type": "type de bloc de contenu inattendu : %T",
@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "Afficher les métadonnées de la vidéo",
"path_to_yaml_config": "Chemin vers le fichier de configuration YAML",
"pattern_not_found_list_available": "modèle '%s' non trouvé. Exécutez 'fabric -l' pour voir les modèles disponibles",
"pattern_invalid_name": "nom de modèle invalide : %q",
"pattern_not_found_no_patterns": "modèle '%s' non trouvé.\n\nAucun modèle n'est installé ! Pour résoudre ce problème :\n • Exécutez 'fabric --setup' pour configurer et télécharger les modèles\n • Ou exécutez 'fabric -U' pour télécharger/mettre à jour les modèles directement",
"pattern_variables_help": "Valeurs pour les variables de motif, ex. -v=#role:expert -v=#points:30",
"patterns_cloning_repository": "Clonage du dépôt %s (chemin : %s)...\\n",
@ -491,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",
@ -504,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",
@ -599,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",
@ -610,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",
@ -628,11 +643,11 @@
"strategy_path_traversal": "le nom de stratégie %q se résout en dehors du répertoire des stratégies",
"stream_help": "Streaming",
"suppress_thinking_tags": "Supprimer le texte encadré par les balises de réflexion",
"template_datetime_error_invalid_number": "invalid number in relative time: %q",
"template_datetime_error_invalid_relative_format": "invalid relative time format",
"template_datetime_error_invalid_unit": "invalid time unit: %q",
"template_datetime_error_relative_requires_value": "relative time requires a value",
"template_datetime_error_unknown_operation": "datetime: unknown operation %q",
"template_datetime_error_invalid_number": "nombre invalide dans le temps relatif : %q",
"template_datetime_error_invalid_relative_format": "format de temps relatif invalide",
"template_datetime_error_invalid_unit": "unité de temps invalide : %q",
"template_datetime_error_relative_requires_value": "le temps relatif nécessite une valeur",
"template_datetime_error_unknown_operation": "datetime: opération inconnue %q",
"template_extension_error": "Erreur d'extension %s : %v",
"template_file_error_expand_home_dir": "fichier : impossible d'étendre le répertoire personnel : %v",
"template_file_error_invalid_line_count": "fichier : nombre de lignes invalide %q",
@ -661,12 +676,12 @@
"template_missing_required_variable": "Variable requise manquante : %s",
"template_plugin_error": "Erreur du plugin %s : %v",
"template_processing_stuck": "Traitement du modèle bloqué - boucle infinie potentielle",
"template_sys_error_env_requires_var": "env: variable name required",
"template_sys_error_home": "failed to get home directory: %v",
"template_sys_error_hostname": "failed to get hostname: %v",
"template_sys_error_pwd": "failed to get working directory: %v",
"template_sys_error_unknown_operation": "sys: unknown operation %q",
"template_sys_error_user": "failed to get current user: %v",
"template_sys_error_env_requires_var": "env: nom de variable requis",
"template_sys_error_home": "impossible d'obtenir le répertoire personnel : %v",
"template_sys_error_hostname": "impossible d'obtenir le nom d'hôte : %v",
"template_sys_error_pwd": "impossible d'obtenir le répertoire de travail : %v",
"template_sys_error_unknown_operation": "sys: opération inconnue %q",
"template_sys_error_user": "impossible d'obtenir l'utilisateur actuel : %v",
"template_text_empty_input": "Texte : entrée vide pour l'opération %q",
"template_text_unknown_operation": "Texte : opération de texte inconnue %q (supportées : upper, lower, title, trim)",
"template_unknown_plugin_namespace": "Espace de noms de plugin inconnu : %s",

View file

@ -54,7 +54,7 @@
"bedrock_aws_access_key_label": "Inserisci il tuo AWS Access Key ID (lascia vuoto per usare la catena di credenziali AWS)",
"bedrock_aws_region_label": "Regione AWS",
"bedrock_aws_secret_key_label": "Inserisci il tuo AWS Secret Access Key (lascia vuoto per usare la catena di credenziali AWS)",
"bedrock_client_not_initialized": "Bedrock client not initialized — run 'fabric --setup' to configure",
"bedrock_client_not_initialized": "client Bedrock non inizializzato — eseguire 'fabric --setup' per configurare",
"bedrock_converse_failed": "bedrock converse fallito per il modello %s: %w",
"bedrock_conversestream_failed": "bedrock conversestream fallito per il modello %s: %w",
"bedrock_empty_response_content": "contenuto della risposta vuoto",
@ -67,21 +67,21 @@
"bedrock_listmodels_fallback": "API ListModels Bedrock fallita, utilizzo della lista statica di fallback",
"bedrock_panic_sendstream": "panic in SendStream: %v",
"bedrock_setup_auth_option_accesskey": " [2] AWS Access Key + Secret Key",
"bedrock_setup_auth_option_apikey": " [1] Bedrock API Key / ABSK token (recommended — same as Claude Code)",
"bedrock_setup_auth_prompt": "Enter 1 or 2",
"bedrock_setup_choose_auth_method": " Choose authentication method:",
"bedrock_setup_choose_model": " Choose default model (unprefixed IDs work in any region; us./eu./ap. are region-specific):",
"bedrock_setup_choose_region": " Choose AWS Region:",
"bedrock_setup_auth_option_apikey": " [1] Chiave API Bedrock / token ABSK (consigliato — la stessa usata da Claude Code)",
"bedrock_setup_auth_prompt": "Inserisci 1 o 2",
"bedrock_setup_choose_auth_method": " Scegli il metodo di autenticazione:",
"bedrock_setup_choose_model": " Scegli il modello predefinito (gli ID senza prefisso funzionano in qualsiasi regione; us./eu./ap. sono specifici per regione):",
"bedrock_setup_choose_region": " Scegli la regione AWS:",
"bedrock_setup_header": "[Bedrock]",
"bedrock_setup_invalid_auth_selection": "invalid selection: %s (enter 1 or 2)",
"bedrock_setup_model_custom_prompt": "Enter model ID (e.g. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Enter a different model ID",
"bedrock_setup_model_prompt": "Enter model number or 0 to type your own",
"bedrock_setup_region_custom_prompt": "Enter custom AWS region (e.g. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Enter a different region",
"bedrock_setup_region_prompt": "Enter region number or 0 for custom",
"bedrock_setup_selected_model": " ✓ Selected model: %s",
"bedrock_setup_use_with": " Use with: fabric -m %s -V Bedrock",
"bedrock_setup_invalid_auth_selection": "selezione non valida: %s (inserisci 1 o 2)",
"bedrock_setup_model_custom_prompt": "Inserisci l'ID del modello (es. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Inserisci un ID modello diverso",
"bedrock_setup_model_prompt": "Inserisci il numero del modello o 0 per digitarne uno tuo",
"bedrock_setup_region_custom_prompt": "Inserisci una regione AWS personalizzata (es. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Inserisci una regione diversa",
"bedrock_setup_region_prompt": "Inserisci il numero della regione o 0 per personalizzarla",
"bedrock_setup_selected_model": " ✓ Modello selezionato: %s",
"bedrock_setup_use_with": " Uso: fabric -m %s -V Bedrock",
"bedrock_unable_load_aws_config": "impossibile caricare la configurazione AWS: %w",
"bedrock_unable_load_aws_config_with_region": "impossibile caricare la configurazione AWS con regione %s: %w",
"bedrock_unexpected_content_block_type": "tipo di blocco contenuto inaspettato: %T",
@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "Output metadati video",
"path_to_yaml_config": "Percorso del file di configurazione YAML",
"pattern_not_found_list_available": "pattern '%s' non trovato. Esegui 'fabric -l' per vedere i pattern disponibili",
"pattern_invalid_name": "nome pattern non valido: %q",
"pattern_not_found_no_patterns": "pattern '%s' non trovato.\n\nNessun pattern installato! Per risolvere:\n • Esegui 'fabric --setup' per configurare e scaricare i pattern\n • Oppure esegui 'fabric -U' per scaricare/aggiornare i pattern direttamente",
"pattern_variables_help": "Valori per le variabili pattern, es. -v=#role:expert -v=#points:30",
"patterns_cloning_repository": "Clonazione del repository %s (percorso: %s)...\\n",
@ -491,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",
@ -504,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",
@ -599,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",
@ -610,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",
@ -628,11 +643,11 @@
"strategy_path_traversal": "il nome della strategia %q si risolve al di fuori della directory delle strategie",
"stream_help": "Streaming",
"suppress_thinking_tags": "Sopprimi testo racchiuso in tag di pensiero",
"template_datetime_error_invalid_number": "invalid number in relative time: %q",
"template_datetime_error_invalid_relative_format": "invalid relative time format",
"template_datetime_error_invalid_unit": "invalid time unit: %q",
"template_datetime_error_relative_requires_value": "relative time requires a value",
"template_datetime_error_unknown_operation": "datetime: unknown operation %q",
"template_datetime_error_invalid_number": "numero non valido nel tempo relativo: %q",
"template_datetime_error_invalid_relative_format": "formato di tempo relativo non valido",
"template_datetime_error_invalid_unit": "unità di tempo non valida: %q",
"template_datetime_error_relative_requires_value": "il tempo relativo richiede un valore",
"template_datetime_error_unknown_operation": "datetime: operazione sconosciuta %q",
"template_extension_error": "Errore dell'estensione %s: %v",
"template_file_error_expand_home_dir": "file: impossibile espandere la directory home: %v",
"template_file_error_invalid_line_count": "file: numero di righe non valido %q",
@ -661,12 +676,12 @@
"template_missing_required_variable": "Variabile richiesta mancante: %s",
"template_plugin_error": "Errore del plugin %s: %v",
"template_processing_stuck": "Elaborazione del modello bloccata - possibile ciclo infinito",
"template_sys_error_env_requires_var": "env: variable name required",
"template_sys_error_home": "failed to get home directory: %v",
"template_sys_error_hostname": "failed to get hostname: %v",
"template_sys_error_pwd": "failed to get working directory: %v",
"template_sys_error_unknown_operation": "sys: unknown operation %q",
"template_sys_error_user": "failed to get current user: %v",
"template_sys_error_env_requires_var": "env: nome della variabile richiesto",
"template_sys_error_home": "impossibile ottenere la directory home: %v",
"template_sys_error_hostname": "impossibile ottenere il nome host: %v",
"template_sys_error_pwd": "impossibile ottenere la directory di lavoro: %v",
"template_sys_error_unknown_operation": "sys: operazione sconosciuta %q",
"template_sys_error_user": "impossibile ottenere l'utente corrente: %v",
"template_text_empty_input": "Testo: input vuoto per l'operazione %q",
"template_text_unknown_operation": "Testo: operazione di testo sconosciuta %q (supportate: upper, lower, title, trim)",
"template_unknown_plugin_namespace": "Namespace del plugin sconosciuto: %s",

View file

@ -54,7 +54,7 @@
"bedrock_aws_access_key_label": "AWS Access Key IDを入力してくださいAWS認証チェーンを使用する場合は空のままにしてください",
"bedrock_aws_region_label": "AWSリージョン",
"bedrock_aws_secret_key_label": "AWS Secret Access Keyを入力してくださいAWS認証チェーンを使用する場合は空のままにしてください",
"bedrock_client_not_initialized": "Bedrock client not initialized — run 'fabric --setup' to configure",
"bedrock_client_not_initialized": "Bedrockクライアントが初期化されていません — 設定するには 'fabric --setup' を実行してください",
"bedrock_converse_failed": "モデル %s のbedrock converseが失敗しました: %w",
"bedrock_conversestream_failed": "モデル %s のbedrock conversestreamが失敗しました: %w",
"bedrock_empty_response_content": "空のレスポンスコンテンツ",
@ -67,21 +67,21 @@
"bedrock_listmodels_fallback": "Bedrock ListModels APIが失敗しました、静的フォールバックリストを使用",
"bedrock_panic_sendstream": "SendStreamでパニックが発生しました: %v",
"bedrock_setup_auth_option_accesskey": " [2] AWS Access Key + Secret Key",
"bedrock_setup_auth_option_apikey": " [1] Bedrock API Key / ABSK token (recommended — same as Claude Code)",
"bedrock_setup_auth_prompt": "Enter 1 or 2",
"bedrock_setup_choose_auth_method": " Choose authentication method:",
"bedrock_setup_choose_model": " Choose default model (unprefixed IDs work in any region; us./eu./ap. are region-specific):",
"bedrock_setup_choose_region": " Choose AWS Region:",
"bedrock_setup_auth_option_apikey": " [1] Bedrock APIキー / ABSKトークン推奨 — Claude Code と同じ)",
"bedrock_setup_auth_prompt": "1 または 2 を入力してください",
"bedrock_setup_choose_auth_method": " 認証方法を選択してください:",
"bedrock_setup_choose_model": " デフォルトモデルを選択してくださいプレフィックスなしのIDはどのリージョンでも使用できます。us./eu./ap. はリージョン固有です):",
"bedrock_setup_choose_region": " AWSリージョンを選択してください:",
"bedrock_setup_header": "[Bedrock]",
"bedrock_setup_invalid_auth_selection": "invalid selection: %s (enter 1 or 2)",
"bedrock_setup_model_custom_prompt": "Enter model ID (e.g. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Enter a different model ID",
"bedrock_setup_model_prompt": "Enter model number or 0 to type your own",
"bedrock_setup_region_custom_prompt": "Enter custom AWS region (e.g. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Enter a different region",
"bedrock_setup_region_prompt": "Enter region number or 0 for custom",
"bedrock_setup_selected_model": " ✓ Selected model: %s",
"bedrock_setup_use_with": " Use with: fabric -m %s -V Bedrock",
"bedrock_setup_invalid_auth_selection": "無効な選択: %s1 または 2 を入力してください)",
"bedrock_setup_model_custom_prompt": "モデルIDを入力してください例: anthropic.claude-opus-4-6-v1",
"bedrock_setup_model_option_custom": " [0] 別のモデルIDを入力",
"bedrock_setup_model_prompt": "モデル番号を入力、または 0 で任意のIDを入力",
"bedrock_setup_region_custom_prompt": "カスタムAWSリージョンを入力してください例: us-east-1",
"bedrock_setup_region_option_custom": " [0] 別のリージョンを入力",
"bedrock_setup_region_prompt": "リージョン番号を入力、または 0 でカスタム指定",
"bedrock_setup_selected_model": " ✓ 選択したモデル: %s",
"bedrock_setup_use_with": " 使用方法: fabric -m %s -V Bedrock",
"bedrock_unable_load_aws_config": "AWS設定を読み込めませんでした: %w",
"bedrock_unable_load_aws_config_with_region": "リージョン %s でAWS設定を読み込めませんでした: %w",
"bedrock_unexpected_content_block_type": "予期しないコンテンツブロックタイプ: %T",
@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "動画メタデータを出力",
"path_to_yaml_config": "YAML設定ファイルのパス",
"pattern_not_found_list_available": "パターン '%s' が見つかりません。'fabric -l'で利用可能なパターンを確認してください",
"pattern_invalid_name": "無効なパターン名: %q",
"pattern_not_found_no_patterns": "パターン '%s' が見つかりません。\n\nパターンがインストールされていません解決するには:\n • 'fabric --setup'を実行してパターンを設定・ダウンロード\n • または'fabric -U'を実行してパターンをダウンロード/更新",
"pattern_variables_help": "パターン変数の値、例:-v=#role:expert -v=#points:30",
"patterns_cloning_repository": "リポジトリ %s をクローン中 (パス: %s)...\\n",
@ -491,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": "名前で登録済み拡張機能を削除",
@ -504,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": "頻度ペナルティを設定",
@ -599,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",
@ -610,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",
@ -628,11 +643,11 @@
"strategy_path_traversal": "戦略名 %q が戦略ディレクトリの外部に解決されます",
"stream_help": "ストリーミング",
"suppress_thinking_tags": "思考タグで囲まれたテキストを抑制",
"template_datetime_error_invalid_number": "invalid number in relative time: %q",
"template_datetime_error_invalid_relative_format": "invalid relative time format",
"template_datetime_error_invalid_unit": "invalid time unit: %q",
"template_datetime_error_relative_requires_value": "relative time requires a value",
"template_datetime_error_unknown_operation": "datetime: unknown operation %q",
"template_datetime_error_invalid_number": "相対時間の数値が無効です: %q",
"template_datetime_error_invalid_relative_format": "相対時間の形式が無効です",
"template_datetime_error_invalid_unit": "時間単位が無効です: %q",
"template_datetime_error_relative_requires_value": "相対時間には値が必要です",
"template_datetime_error_unknown_operation": "datetime: 不明な操作 %q",
"template_extension_error": "拡張機能%sエラー: %v",
"template_file_error_expand_home_dir": "file: ホームディレクトリを展開できませんでした: %v",
"template_file_error_invalid_line_count": "file: 無効な行数 %q",
@ -649,7 +664,7 @@
"template_file_log_cleaned_path": "File: 正規化後のパス %q",
"template_file_log_exists_for_path": "File: パス %q の exists=%v",
"template_file_log_modified_for_path": "File: パス %q の modified=%q",
"template_file_log_operation_value": "File: operation=%q value=%q",
"template_file_log_operation_value": "ファイル: operation=%q value=%q",
"template_file_log_read_bytes": "File: %d バイトを読み取りました",
"template_file_log_read_total_return_last": "File: 合計 %d 行を読み取り、最後の %d 行を返します",
"template_file_log_reading_last_lines": "File: %q から最後の %d 行を読み取り中",
@ -661,12 +676,12 @@
"template_missing_required_variable": "必須変数が不足しています: %s",
"template_plugin_error": "プラグイン%sエラー: %v",
"template_processing_stuck": "テンプレート処理が停止 - 無限ループの可能性",
"template_sys_error_env_requires_var": "env: variable name required",
"template_sys_error_home": "failed to get home directory: %v",
"template_sys_error_hostname": "failed to get hostname: %v",
"template_sys_error_pwd": "failed to get working directory: %v",
"template_sys_error_unknown_operation": "sys: unknown operation %q",
"template_sys_error_user": "failed to get current user: %v",
"template_sys_error_env_requires_var": "env: 変数名が必要です",
"template_sys_error_home": "ホームディレクトリの取得に失敗しました: %v",
"template_sys_error_hostname": "ホスト名の取得に失敗しました: %v",
"template_sys_error_pwd": "作業ディレクトリの取得に失敗しました: %v",
"template_sys_error_unknown_operation": "sys: 不明な操作 %q",
"template_sys_error_user": "現在のユーザーの取得に失敗しました: %v",
"template_text_empty_input": "テキスト: 操作%qに対する入力が空です",
"template_text_unknown_operation": "テキスト: 不明なテキスト操作%q (対応: upper, lower, title, trim)",
"template_unknown_plugin_namespace": "不明なプラグイン名前空間: %s",

View file

@ -54,7 +54,7 @@
"bedrock_aws_access_key_label": "Wprowadź swój AWS Access Key ID (pozostaw puste, aby użyć łańcucha uwierzytelniania AWS)",
"bedrock_aws_region_label": "Region AWS",
"bedrock_aws_secret_key_label": "Wprowadź swój AWS Secret Access Key (pozostaw puste, aby użyć łańcucha uwierzytelniania AWS)",
"bedrock_client_not_initialized": "Bedrock client not initialized — run 'fabric --setup' to configure",
"bedrock_client_not_initialized": "klient Bedrock nie jest zainicjalizowany — uruchom 'fabric --setup', aby skonfigurować",
"bedrock_converse_failed": "bedrock converse nie powiodło się dla modelu %s: %w",
"bedrock_conversestream_failed": "bedrock conversestream nie powiodło się dla modelu %s: %w",
"bedrock_empty_response_content": "pusta zawartość odpowiedzi",
@ -67,21 +67,21 @@
"bedrock_listmodels_fallback": "API ListModels Bedrock nie powiodło się, używam statycznej listy zapasowej",
"bedrock_panic_sendstream": "panika w SendStream: %v",
"bedrock_setup_auth_option_accesskey": " [2] AWS Access Key + Secret Key",
"bedrock_setup_auth_option_apikey": " [1] Bedrock API Key / ABSK token (recommended — same as Claude Code)",
"bedrock_setup_auth_prompt": "Enter 1 or 2",
"bedrock_setup_choose_auth_method": " Choose authentication method:",
"bedrock_setup_choose_model": " Choose default model (unprefixed IDs work in any region; us./eu./ap. are region-specific):",
"bedrock_setup_choose_region": " Choose AWS Region:",
"bedrock_setup_auth_option_apikey": " [1] Klucz API Bedrock / token ABSK (zalecane — taki sam jak w Claude Code)",
"bedrock_setup_auth_prompt": "Wprowadź 1 lub 2",
"bedrock_setup_choose_auth_method": " Wybierz metodę uwierzytelniania:",
"bedrock_setup_choose_model": " Wybierz model domyślny (identyfikatory bez prefiksu działają w każdym regionie; us./eu./ap. są specyficzne dla regionu):",
"bedrock_setup_choose_region": " Wybierz region AWS:",
"bedrock_setup_header": "[Bedrock]",
"bedrock_setup_invalid_auth_selection": "invalid selection: %s (enter 1 or 2)",
"bedrock_setup_model_custom_prompt": "Enter model ID (e.g. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Enter a different model ID",
"bedrock_setup_model_prompt": "Enter model number or 0 to type your own",
"bedrock_setup_region_custom_prompt": "Enter custom AWS region (e.g. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Enter a different region",
"bedrock_setup_region_prompt": "Enter region number or 0 for custom",
"bedrock_setup_selected_model": " ✓ Selected model: %s",
"bedrock_setup_use_with": " Use with: fabric -m %s -V Bedrock",
"bedrock_setup_invalid_auth_selection": "nieprawidłowy wybór: %s (wprowadź 1 lub 2)",
"bedrock_setup_model_custom_prompt": "Wprowadź identyfikator modelu (np. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Wprowadź inny identyfikator modelu",
"bedrock_setup_model_prompt": "Wprowadź numer modelu lub 0, aby podać własny",
"bedrock_setup_region_custom_prompt": "Wprowadź własny region AWS (np. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Wprowadź inny region",
"bedrock_setup_region_prompt": "Wprowadź numer regionu lub 0, aby podać własny",
"bedrock_setup_selected_model": " ✓ Wybrany model: %s",
"bedrock_setup_use_with": " Użycie: fabric -m %s -V Bedrock",
"bedrock_unable_load_aws_config": "nie można załadować konfiguracji AWS: %w",
"bedrock_unable_load_aws_config_with_region": "nie można załadować konfiguracji AWS dla regionu %s: %w",
"bedrock_unexpected_content_block_type": "nieoczekiwany typ bloku zawartości: %T",
@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "Wyprowadź metadane wideo",
"path_to_yaml_config": "Ścieżka do pliku konfiguracyjnego YAML",
"pattern_not_found_list_available": "wzorzec '%s' nie został znaleziony. Uruchom 'fabric -l', aby zobaczyć dostępne wzorce",
"pattern_invalid_name": "nieprawidłowa nazwa wzorca: %q",
"pattern_not_found_no_patterns": "wzorzec '%s' nie został znaleziony.\n\nNie zainstalowano żadnych wzorców! Aby to naprawić:\n • Uruchom 'fabric --setup', aby skonfigurować i pobrać wzorce\n • Lub uruchom 'fabric -U', aby bezpośrednio pobrać/zaktualizować wzorce",
"pattern_variables_help": "Wartości dla zmiennych wzorców, np. -v=#role:ekspert -v=#points:30",
"patterns_cloning_repository": "Klonowanie repozytorium %s (ścieżka: %s)...\n",
@ -491,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",
@ -504,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",
@ -599,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",
@ -610,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",
@ -649,7 +664,7 @@
"template_file_log_cleaned_path": "File: oczyszczona ścieżka %q",
"template_file_log_exists_for_path": "File: exists=%v dla ścieżki %q",
"template_file_log_modified_for_path": "File: modified=%q dla ścieżki %q",
"template_file_log_operation_value": "File: operation=%q value=%q",
"template_file_log_operation_value": "Plik: operation=%q value=%q",
"template_file_log_read_bytes": "File: odczytano %d bajtów",
"template_file_log_read_total_return_last": "File: odczytano łącznie %d linii, zwracam ostatnie %d",
"template_file_log_reading_last_lines": "File: odczytywanie ostatnich %d linii z %q",

View file

@ -54,7 +54,7 @@
"bedrock_aws_access_key_label": "Digite seu AWS Access Key ID (deixe vazio para usar a cadeia de credenciais AWS)",
"bedrock_aws_region_label": "Regiao AWS",
"bedrock_aws_secret_key_label": "Digite seu AWS Secret Access Key (deixe vazio para usar a cadeia de credenciais AWS)",
"bedrock_client_not_initialized": "Bedrock client not initialized — run 'fabric --setup' to configure",
"bedrock_client_not_initialized": "cliente Bedrock não inicializado — execute 'fabric --setup' para configurar",
"bedrock_converse_failed": "bedrock converse falhou para o modelo %s: %w",
"bedrock_conversestream_failed": "bedrock conversestream falhou para o modelo %s: %w",
"bedrock_empty_response_content": "conteudo de resposta vazio",
@ -67,21 +67,21 @@
"bedrock_listmodels_fallback": "API ListModels do Bedrock falhou, usando lista estática de fallback",
"bedrock_panic_sendstream": "panico no SendStream: %v",
"bedrock_setup_auth_option_accesskey": " [2] AWS Access Key + Secret Key",
"bedrock_setup_auth_option_apikey": " [1] Bedrock API Key / ABSK token (recommended — same as Claude Code)",
"bedrock_setup_auth_prompt": "Enter 1 or 2",
"bedrock_setup_choose_auth_method": " Choose authentication method:",
"bedrock_setup_choose_model": " Choose default model (unprefixed IDs work in any region; us./eu./ap. are region-specific):",
"bedrock_setup_choose_region": " Choose AWS Region:",
"bedrock_setup_auth_option_apikey": " [1] Chave API Bedrock / token ABSK (recomendado — a mesma usada pelo Claude Code)",
"bedrock_setup_auth_prompt": "Digite 1 ou 2",
"bedrock_setup_choose_auth_method": " Escolha o método de autenticação:",
"bedrock_setup_choose_model": " Escolha o modelo padrão (IDs sem prefixo funcionam em qualquer região; us./eu./ap. são específicos de região):",
"bedrock_setup_choose_region": " Escolha a região AWS:",
"bedrock_setup_header": "[Bedrock]",
"bedrock_setup_invalid_auth_selection": "invalid selection: %s (enter 1 or 2)",
"bedrock_setup_model_custom_prompt": "Enter model ID (e.g. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Enter a different model ID",
"bedrock_setup_model_prompt": "Enter model number or 0 to type your own",
"bedrock_setup_region_custom_prompt": "Enter custom AWS region (e.g. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Enter a different region",
"bedrock_setup_region_prompt": "Enter region number or 0 for custom",
"bedrock_setup_selected_model": " ✓ Selected model: %s",
"bedrock_setup_use_with": " Use with: fabric -m %s -V Bedrock",
"bedrock_setup_invalid_auth_selection": "seleção inválida: %s (digite 1 ou 2)",
"bedrock_setup_model_custom_prompt": "Digite o ID do modelo (ex. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Digitar um ID de modelo diferente",
"bedrock_setup_model_prompt": "Digite o número do modelo ou 0 para informar o seu",
"bedrock_setup_region_custom_prompt": "Digite uma região AWS personalizada (ex. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Digitar uma região diferente",
"bedrock_setup_region_prompt": "Digite o número da região ou 0 para personalizar",
"bedrock_setup_selected_model": " ✓ Modelo selecionado: %s",
"bedrock_setup_use_with": " Uso: fabric -m %s -V Bedrock",
"bedrock_unable_load_aws_config": "nao foi possivel carregar a configuracao AWS: %w",
"bedrock_unable_load_aws_config_with_region": "nao foi possivel carregar a configuracao AWS com regiao %s: %w",
"bedrock_unexpected_content_block_type": "tipo de bloco de conteudo inesperado: %T",
@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "Exibir metadados do vídeo",
"path_to_yaml_config": "Caminho para arquivo de configuração YAML",
"pattern_not_found_list_available": "padrão '%s' não encontrado. Execute 'fabric -l' para ver os padrões disponíveis",
"pattern_invalid_name": "nome de padrão inválido: %q",
"pattern_not_found_no_patterns": "padrão '%s' não encontrado.\n\nNenhum padrão instalado! Para resolver:\n • Execute 'fabric --setup' para configurar e baixar padrões\n • Ou execute 'fabric -U' para baixar/atualizar padrões diretamente",
"pattern_variables_help": "Valores para variáveis do padrão, ex. -v=#role:expert -v=#points:30",
"patterns_cloning_repository": "Clonando repositório %s (caminho: %s)...\\n",
@ -491,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",
@ -504,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",
@ -599,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",
@ -610,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",
@ -628,11 +643,11 @@
"strategy_path_traversal": "o nome da estratégia %q resolve fora do diretório de estratégias",
"stream_help": "Streaming",
"suppress_thinking_tags": "Suprimir texto contido em tags de pensamento",
"template_datetime_error_invalid_number": "invalid number in relative time: %q",
"template_datetime_error_invalid_relative_format": "invalid relative time format",
"template_datetime_error_invalid_unit": "invalid time unit: %q",
"template_datetime_error_relative_requires_value": "relative time requires a value",
"template_datetime_error_unknown_operation": "datetime: unknown operation %q",
"template_datetime_error_invalid_number": "número inválido no tempo relativo: %q",
"template_datetime_error_invalid_relative_format": "formato de tempo relativo inválido",
"template_datetime_error_invalid_unit": "unidade de tempo inválida: %q",
"template_datetime_error_relative_requires_value": "o tempo relativo requer um valor",
"template_datetime_error_unknown_operation": "datetime: operação desconhecida %q",
"template_extension_error": "Erro na extensão %s: %v",
"template_file_error_expand_home_dir": "arquivo: não foi possível expandir o diretório home: %v",
"template_file_error_invalid_line_count": "arquivo: contagem de linhas inválida %q",
@ -661,12 +676,12 @@
"template_missing_required_variable": "Variável obrigatória ausente: %s",
"template_plugin_error": "Erro no plugin %s: %v",
"template_processing_stuck": "Processamento do modelo travado - possível loop infinito",
"template_sys_error_env_requires_var": "env: variable name required",
"template_sys_error_home": "failed to get home directory: %v",
"template_sys_error_hostname": "failed to get hostname: %v",
"template_sys_error_pwd": "failed to get working directory: %v",
"template_sys_error_unknown_operation": "sys: unknown operation %q",
"template_sys_error_user": "failed to get current user: %v",
"template_sys_error_env_requires_var": "env: nome da variável obrigatório",
"template_sys_error_home": "falha ao obter o diretório home: %v",
"template_sys_error_hostname": "falha ao obter o nome do host: %v",
"template_sys_error_pwd": "falha ao obter o diretório de trabalho: %v",
"template_sys_error_unknown_operation": "sys: operação desconhecida %q",
"template_sys_error_user": "falha ao obter o usuário atual: %v",
"template_text_empty_input": "Texto: entrada vazia para a operação %q",
"template_text_unknown_operation": "Texto: operação de texto desconhecida %q (suportadas: upper, lower, title, trim)",
"template_unknown_plugin_namespace": "Namespace de plugin desconhecido: %s",

View file

@ -54,7 +54,7 @@
"bedrock_aws_access_key_label": "Digite o seu AWS Access Key ID (deixe vazio para usar a cadeia de credenciais AWS)",
"bedrock_aws_region_label": "Regiao AWS",
"bedrock_aws_secret_key_label": "Digite o seu AWS Secret Access Key (deixe vazio para usar a cadeia de credenciais AWS)",
"bedrock_client_not_initialized": "Bedrock client not initialized — run 'fabric --setup' to configure",
"bedrock_client_not_initialized": "cliente Bedrock não inicializado — execute 'fabric --setup' para configurar",
"bedrock_converse_failed": "bedrock converse falhou para o modelo %s: %w",
"bedrock_conversestream_failed": "bedrock conversestream falhou para o modelo %s: %w",
"bedrock_empty_response_content": "conteudo de resposta vazio",
@ -67,21 +67,21 @@
"bedrock_listmodels_fallback": "API ListModels do Bedrock falhou, usando lista estática de recurso",
"bedrock_panic_sendstream": "panico no SendStream: %v",
"bedrock_setup_auth_option_accesskey": " [2] AWS Access Key + Secret Key",
"bedrock_setup_auth_option_apikey": " [1] Bedrock API Key / ABSK token (recommended — same as Claude Code)",
"bedrock_setup_auth_prompt": "Enter 1 or 2",
"bedrock_setup_choose_auth_method": " Choose authentication method:",
"bedrock_setup_choose_model": " Choose default model (unprefixed IDs work in any region; us./eu./ap. are region-specific):",
"bedrock_setup_choose_region": " Choose AWS Region:",
"bedrock_setup_auth_option_apikey": " [1] Chave API Bedrock / token ABSK (recomendado — a mesma usada pelo Claude Code)",
"bedrock_setup_auth_prompt": "Introduza 1 ou 2",
"bedrock_setup_choose_auth_method": " Escolha o método de autenticação:",
"bedrock_setup_choose_model": " Escolha o modelo predefinido (IDs sem prefixo funcionam em qualquer região; us./eu./ap. são específicos de região):",
"bedrock_setup_choose_region": " Escolha a região AWS:",
"bedrock_setup_header": "[Bedrock]",
"bedrock_setup_invalid_auth_selection": "invalid selection: %s (enter 1 or 2)",
"bedrock_setup_model_custom_prompt": "Enter model ID (e.g. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Enter a different model ID",
"bedrock_setup_model_prompt": "Enter model number or 0 to type your own",
"bedrock_setup_region_custom_prompt": "Enter custom AWS region (e.g. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Enter a different region",
"bedrock_setup_region_prompt": "Enter region number or 0 for custom",
"bedrock_setup_selected_model": " ✓ Selected model: %s",
"bedrock_setup_use_with": " Use with: fabric -m %s -V Bedrock",
"bedrock_setup_invalid_auth_selection": "seleção inválida: %s (introduza 1 ou 2)",
"bedrock_setup_model_custom_prompt": "Introduza o ID do modelo (ex. anthropic.claude-opus-4-6-v1)",
"bedrock_setup_model_option_custom": " [0] Introduzir um ID de modelo diferente",
"bedrock_setup_model_prompt": "Introduza o número do modelo ou 0 para indicar o seu",
"bedrock_setup_region_custom_prompt": "Introduza uma região AWS personalizada (ex. us-east-1)",
"bedrock_setup_region_option_custom": " [0] Introduzir uma região diferente",
"bedrock_setup_region_prompt": "Introduza o número da região ou 0 para personalizar",
"bedrock_setup_selected_model": " ✓ Modelo selecionado: %s",
"bedrock_setup_use_with": " Uso: fabric -m %s -V Bedrock",
"bedrock_unable_load_aws_config": "nao foi possivel carregar a configuracao AWS: %w",
"bedrock_unable_load_aws_config_with_region": "nao foi possivel carregar a configuracao AWS com regiao %s: %w",
"bedrock_unexpected_content_block_type": "tipo de bloco de conteudo inesperado: %T",
@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "Mostrar metadados do vídeo",
"path_to_yaml_config": "Caminho para ficheiro de configuração YAML",
"pattern_not_found_list_available": "padrão '%s' não encontrado. Execute 'fabric -l' para ver os padrões disponíveis",
"pattern_invalid_name": "nome de padrão inválido: %q",
"pattern_not_found_no_patterns": "padrão '%s' não encontrado.\n\nNenhum padrão instalado! Para resolver:\n • Execute 'fabric --setup' para configurar e descarregar padrões\n • Ou execute 'fabric -U' para descarregar/atualizar padrões diretamente",
"pattern_variables_help": "Valores para variáveis de padrão, ex. -v=#role:expert -v=#points:30",
"patterns_cloning_repository": "A clonar repositório %s (caminho: %s)...\\n",
@ -491,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",
@ -504,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",
@ -599,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",
@ -610,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",
@ -628,11 +643,11 @@
"strategy_path_traversal": "o nome da estratégia %q resolve fora do diretório de estratégias",
"stream_help": "Streaming",
"suppress_thinking_tags": "Suprimir texto contido em tags de pensamento",
"template_datetime_error_invalid_number": "invalid number in relative time: %q",
"template_datetime_error_invalid_relative_format": "invalid relative time format",
"template_datetime_error_invalid_unit": "invalid time unit: %q",
"template_datetime_error_relative_requires_value": "relative time requires a value",
"template_datetime_error_unknown_operation": "datetime: unknown operation %q",
"template_datetime_error_invalid_number": "número inválido no tempo relativo: %q",
"template_datetime_error_invalid_relative_format": "formato de tempo relativo inválido",
"template_datetime_error_invalid_unit": "unidade de tempo inválida: %q",
"template_datetime_error_relative_requires_value": "o tempo relativo requer um valor",
"template_datetime_error_unknown_operation": "datetime: operação desconhecida %q",
"template_extension_error": "Erro na extensão %s: %v",
"template_file_error_expand_home_dir": "ficheiro: não foi possível expandir a diretoria home: %v",
"template_file_error_invalid_line_count": "ficheiro: contagem de linhas inválida %q",
@ -661,12 +676,12 @@
"template_missing_required_variable": "Variável obrigatória em falta: %s",
"template_plugin_error": "Erro no plugin %s: %v",
"template_processing_stuck": "Processamento do modelo bloqueado - possível ciclo infinito",
"template_sys_error_env_requires_var": "env: variable name required",
"template_sys_error_home": "failed to get home directory: %v",
"template_sys_error_hostname": "failed to get hostname: %v",
"template_sys_error_pwd": "failed to get working directory: %v",
"template_sys_error_unknown_operation": "sys: unknown operation %q",
"template_sys_error_user": "failed to get current user: %v",
"template_sys_error_env_requires_var": "env: nome da variável obrigatório",
"template_sys_error_home": "falha ao obter o diretório pessoal: %v",
"template_sys_error_hostname": "falha ao obter o nome do host: %v",
"template_sys_error_pwd": "falha ao obter o diretório de trabalho: %v",
"template_sys_error_unknown_operation": "sys: operação desconhecida %q",
"template_sys_error_user": "falha ao obter o utilizador atual: %v",
"template_text_empty_input": "Texto: entrada vazia para a operação %q",
"template_text_unknown_operation": "Texto: operação de texto desconhecida %q (suportadas: upper, lower, title, trim)",
"template_unknown_plugin_namespace": "Espaço de nomes do plugin desconhecido: %s",

View 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ıı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ıı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ıı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ıı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ıı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ıı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"
}

View file

@ -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",
@ -418,6 +426,7 @@
"output_video_metadata": "输出视频元数据",
"path_to_yaml_config": "YAML 配置文件路径",
"pattern_not_found_list_available": "未找到模式 '%s'。运行 'fabric -l' 查看可用模式",
"pattern_invalid_name": "无效的模式名称:%q",
"pattern_not_found_no_patterns": "未找到模式 '%s'。\n\n未安装任何模式要解决此问题\n • 运行 'fabric --setup' 配置并下载模式\n • 或运行 'fabric -U' 直接下载/更新模式",
"pattern_variables_help": "模式变量的值,例如 -v=#role:expert -v=#points:30",
"patterns_cloning_repository": "正在克隆仓库 %s至路径%s...\\n",
@ -491,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": "按名称删除已注册的扩展",
@ -504,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": "设置频率惩罚",
@ -599,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",
@ -610,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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -223,6 +223,7 @@ func (o *Client) NeedsRawMode(modelName string) bool {
openaiModelsPrefixes := []string{
"glm",
"gpt-5",
"gpt-6",
"o1",
"o3",
"o4",

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -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,6 +123,15 @@ func (o *PatternsEntity) applyVariables(
// retrieves a pattern from the database by name
func (o *PatternsEntity) getFromDB(name string) (ret *Pattern, err error) {
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
if o.CustomPatternsDir != "" {
customPatternPath := filepath.Join(o.CustomPatternsDir, name, o.SystemPatternFile)
@ -279,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)
}

View file

@ -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-*")
@ -332,6 +416,47 @@ 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()
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)
})
}
}
// 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)
}
}
func TestPatternsEntity_CustomPatternsEmpty(t *testing.T) {
// Test behavior when custom patterns directory is empty or doesn't exist
mainDir, err := os.MkdirTemp("", "test-main-patterns-*")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View 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())
}
}

View file

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

View file

@ -1 +1 @@
"1.4.461"
"1.4.478"

190
scripts/audit-patterns.sh Executable file
View 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

View file

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

View file

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

View file

@ -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 38 specific, lowercase tags that describe the content. Prefer concrete concepts over vague categories. Avoid single-word generic tags like \"notes\" or \"text\". - Generate 13 aliases: alternative titles or short names someone might search for. - Write a one-sentence summary (1525 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:"
}
]
}

View file

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

View file

@ -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'
}
},
{

View file

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

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because it is too large Load diff

48
web/pnpm-workspace.yaml Normal file
View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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');
});
});

View file

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

View file

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

View 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/);
});
});

View file

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

View file

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

View file

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

View 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