Compare commits

...

15 Commits

Author SHA1 Message Date
github-actions[bot]
2200b6ea08 Update version to v1.4.200 and commit 2025-06-11 21:45:09 +00:00
Eugen Eisler
82f9ebaf99 Merge pull request #1507 from ksylvan/0611-youtube-fix
Refactor: No more web scraping, just use yt-dlp
2025-06-11 23:43:33 +02:00
Kayvan Sylvan
704ad3067a refactor: replace web scraping with yt-dlp for YouTube transcript extraction
## CHANGES

- Remove unreliable YouTube API scraping methods
- Add yt-dlp integration for transcript extraction
- Implement VTT subtitle parsing functionality
- Add timestamp preservation for transcripts
- Remove soup HTML parsing dependency
- Add error handling for missing yt-dlp
- Create temporary directory management
- Support multiple subtitle format fallbacks
2025-06-11 14:24:40 -07:00
github-actions[bot]
6f7e3c04d7 Update version to v1.4.199 and commit 2025-06-11 20:27:06 +00:00
Eugen Eisler
79f763456e Merge pull request #1506 from danielmiessler/feat/antropic_tool
fix: fix web search tool location
2025-06-11 22:25:22 +02:00
github-actions[bot]
8e7373b308 Update version to v1.4.198 and commit 2025-06-11 18:51:13 +00:00
Eugen Eisler
7a39742507 Merge pull request #1504 from marcas756/fix/ollama-hardcoded-timeout
fix: Add configurable HTTP timeout for Ollama client
2025-06-11 20:49:41 +02:00
github-actions[bot]
cea218e61e Update version to v1.4.197 and commit 2025-06-11 18:41:32 +00:00
Eugen Eisler
f673f424da Merge pull request #1502 from danielmiessler/feat/antropic_tool
Feat/antropic tool
2025-06-11 20:40:00 +02:00
Marco Bacchi
0ae41116aa fix: Add configurable HTTP timeout for Ollama client
Add a new setup question to configure the HTTP timeout duration for
Ollama requests. The default value is set to 20 minutes.
2025-06-11 20:36:57 +02:00
Eugen Eisler
29f19fce51 Merge pull request #1499 from noamsiegel/improve-create-prd-pattern
feat: Enhance the PRD Generator's identity and purpose
2025-06-11 18:04:53 +02:00
Eugen Eisler
62ed5d2b9a Merge pull request #1497 from ksylvan/0608-analyze-terraform-plan
feat: add Terraform plan analyzer pattern for infrastructure changes
2025-06-11 18:00:55 +02:00
GitButler
836e4c4fab GitButler Workspace Commit
This is a merge commit the virtual branches in your workspace.

Due to GitButler managing multiple virtual branches, you cannot switch back and
forth between git branches and virtual branches easily. 

If you switch to another branch, GitButler will need to be reinitialized.
If you commit on this branch, GitButler will throw it away.

Here are the branches that are currently applied:
 - improve-create-prd (refs/gitbutler/improve-create-prd)
For more information about what we're doing here, check out our docs:
https://docs.gitbutler.com/features/virtual-branches/integration-branch
2025-06-09 18:24:48 -07:00
Noam Siegel
946c1af42d feat: Enhance the PRD Generator's identity and purpose
The changes in this commit expand the identity and purpose of the PRD Generator
to provide more clarity on its role and the expected output. The key changes
include:

- Defining the Generator's purpose as transforming product ideas into a
  structured PRD that ensures clarity, alignment, and precision in product
  planning and execution.
- Outlining the key sections typically found in a PRD that the Generator should
  cover, such as Overview, Objectives, Target Audience, Features, User Stories,
  Functional and Non-functional Requirements, Success Metrics, and Timeline.
- Providing more detailed instructions on the expected output format, structure,
  and content, including the use of Markdown, labeled sections, bullet points,
  tables, and highlighting of priorities or MVP features.
2025-06-09 18:24:48 -07:00
Kayvan Sylvan
a74585cb14 feat: add Terraform plan analyzer pattern for infrastructure change assessment
- Create new pattern for analyzing Terraform plans
- Add identity defining expert plan analyzer role
- Include focus on security, cost, and compliance
- Define three output sections for summaries
- Specify 20-word sentence summary requirement
- List 10 critical changes with word limits
- Include 5 key takeaways section format
- Add markdown formatting output instructions
- Require numbered lists over bullet points
- Prohibit warnings and duplicate content
2025-06-08 22:49:02 -07:00
8 changed files with 378 additions and 175 deletions

View File

@@ -2,32 +2,32 @@ schema = 3
[mod]
[mod."cloud.google.com/go"]
version = "v0.120.1"
hash = "sha256-yWaLc06rGXk16K53rix8O4uPSX+AOZDgIpIXf+wlh10="
version = "v0.121.2"
hash = "sha256-BCgGHxKti8slH98UDDurtgzX3lgcYEklsmj4ImPpwlc="
[mod."cloud.google.com/go/ai"]
version = "v0.10.2"
hash = "sha256-bsqvdylG8kk+AHtyvMRMv1TOjUmvONAgJ+14mKcwuzs="
version = "v0.12.1"
hash = "sha256-wg3oLMS68E/v7EdNzywbjwEmpk+u6U8LTnIc1pq8edo="
[mod."cloud.google.com/go/auth"]
version = "v0.16.1"
hash = "sha256-rMPMNQh/YM/67b9Grfu0BFccWpS1SRhBepubQqXRAyg="
version = "v0.16.2"
hash = "sha256-BAU9WGFKe0pd5Eu3l/Mbts+QeCOjS+lChr5hrPBCzdA="
[mod."cloud.google.com/go/auth/oauth2adapt"]
version = "v0.2.8"
hash = "sha256-GoXFqAbp1WO1tDj07PF5EyxDYvCBP0l0qwxY2oV2hfc="
[mod."cloud.google.com/go/compute/metadata"]
version = "v0.6.0"
hash = "sha256-E8/cwio4xR8buCryR4HwR7+agb4M3zqgXSm7rBglmIY="
version = "v0.7.0"
hash = "sha256-jJZDW+hibqjMiY8OiJhgJALbGwEq+djLOxfYR7upQyE="
[mod."cloud.google.com/go/longrunning"]
version = "v0.6.7"
hash = "sha256-9I0Nc2KWAEVoxDngNkqFUdASmZIAySfMEELlPh3Q3xA="
[mod."dario.cat/mergo"]
version = "v1.0.1"
hash = "sha256-wcG6+x0k6KzOSlaPA+1RFxa06/RIAePJTAjjuhLbImw="
version = "v1.0.2"
hash = "sha256-p6jdiHlLEfZES8vJnDywG4aVzIe16p0CU6iglglIweA="
[mod."github.com/Microsoft/go-winio"]
version = "v0.6.2"
hash = "sha256-tVNWDUMILZbJvarcl/E7tpSnkn7urqgSHa2Eaka5vSU="
[mod."github.com/ProtonMail/go-crypto"]
version = "v1.2.0"
hash = "sha256-5fKgWUz6BoyFNNZ1OD9QjhBrhNEBCuVfO2WqH+X59oo="
version = "v1.3.0"
hash = "sha256-TUG+C4MyeWglOmiwiW2/NUVurFHXLgEPRd3X9uQ1NGI="
[mod."github.com/anaskhan96/soup"]
version = "v1.2.5"
hash = "sha256-t8yCyK2y7x2qaI/3Yw16q3zVFqu+3acLcPgTr1MIKWg="
@@ -35,8 +35,8 @@ schema = 3
version = "v1.3.3"
hash = "sha256-jv7ZshpSd7FZzKKN6hqlUgiR8C3y85zNIS/hq7g76Ho="
[mod."github.com/anthropics/anthropic-sdk-go"]
version = "v1.2.0"
hash = "sha256-IzSmJBfMB2OAyFOCqwSzwdJMPoTQqJ1rBtKXGrFo2Bc="
version = "v1.4.0"
hash = "sha256-4kwFw9gt/sRIlTo0fC2PbfLnCyc4lCOtmfQelhpORX8="
[mod."github.com/araddon/dateparse"]
version = "v0.0.0-20210429162001-6b43995a97de"
hash = "sha256-UuX84naeRGMsFOgIgRoBHG5sNy1CzBkWPKmd6VbLwFw="
@@ -44,8 +44,8 @@ schema = 3
version = "v0.1.4"
hash = "sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk="
[mod."github.com/bytedance/sonic"]
version = "v1.13.2"
hash = "sha256-IF2qmt4IxTwivMWHUJC8sg6d85/ORb2SWvJ54fvoAMI="
version = "v1.13.3"
hash = "sha256-Nnt5b2NkIvSXhGERQmyI0ka28hbWi7A7Zn3dsAjPcEA="
[mod."github.com/bytedance/sonic/loader"]
version = "v0.2.4"
hash = "sha256-rv9LnePpm4OspSVbfSoVbohXzhu+dxE1BH1gm3mTmTc="
@@ -74,8 +74,8 @@ schema = 3
version = "v1.1.0"
hash = "sha256-2VP6zHEsPi0u2ZYpOTcLulwj1Gsmb6oA19qcP2/AzVM="
[mod."github.com/gin-gonic/gin"]
version = "v1.10.0"
hash = "sha256-esJasHrJtuTBwGPGAoc/XSb428J8va+tPGcZ0gTfsgc="
version = "v1.10.1"
hash = "sha256-D98+chAdjb6JcLPkscOr8TgTW87UqA4h3cnY0XIr16c="
[mod."github.com/go-git/gcfg"]
version = "v1.5.1-0.20230307220236-3a3c6141e376"
hash = "sha256-f4k0gSYuo0/q3WOoTxl2eFaj7WZpdz29ih6CKc8Ude8="
@@ -83,11 +83,11 @@ schema = 3
version = "v5.6.2"
hash = "sha256-VgbxcLkHjiSyRIfKS7E9Sn8OynCrMGUDkwFz6K2TVL4="
[mod."github.com/go-git/go-git/v5"]
version = "v5.16.0"
hash = "sha256-01obPHvt1PG3r8XH8TgnNfcOhaYwWEkJ0TR5QGdZqmE="
version = "v5.16.2"
hash = "sha256-KdOf4KwJAJUIB/EcQH6wc7jpcABCISWur3vOTpAo+/c="
[mod."github.com/go-logr/logr"]
version = "v1.4.2"
hash = "sha256-/W6qGilFlZNTb9Uq48xGZ4IbsVeSwJiAMLw4wiNYHLI="
version = "v1.4.3"
hash = "sha256-Nnp/dEVNMxLp3RSPDHZzGbI8BkSNuZMX0I0cjWKXXLA="
[mod."github.com/go-logr/stdr"]
version = "v1.2.2"
hash = "sha256-rRweAP7XIb4egtT1f2gkz4sYOu7LDHmcJ5iNsJUd0sE="
@@ -116,8 +116,8 @@ schema = 3
version = "v0.0.0-20241129210726-2c02b8208cf8"
hash = "sha256-AdLZ3dJLe/yduoNvZiXugZxNfmwJjNQyQGsIdzYzH74="
[mod."github.com/google/generative-ai-go"]
version = "v0.19.0"
hash = "sha256-x2K1nkRwtne9MeP5B8FpwavYqQx564go5LzmcBJ0KT4="
version = "v0.20.1"
hash = "sha256-9bSpEs4kByhgyTKiHdOY5muYjGBTluA1LvEjw2gSoLI="
[mod."github.com/google/s2a-go"]
version = "v0.1.9"
hash = "sha256-0AdSpSTso4bATmM/9qamWzKrVtOLDf7afvDhoiT/UpA="
@@ -128,8 +128,8 @@ schema = 3
version = "v0.3.6"
hash = "sha256-hPMF0s+X4/ul98GvVuw/ZNOupEXhIDB1yvWymZWYEbU="
[mod."github.com/googleapis/gax-go/v2"]
version = "v2.14.1"
hash = "sha256-iRS/KsAVTePrvTlwA7vKcQnwY6Jz329WdgzFw0hF8wk="
version = "v2.14.2"
hash = "sha256-QyY7wuCkrOJCJIf9Q884KD/BC3vk/QtQLXeLeNPt750="
[mod."github.com/jbenet/go-context"]
version = "v0.0.0-20150711004518-d14ea06fba99"
hash = "sha256-VANNCWNNpARH/ILQV9sCQsBWgyL2iFT+4AHZREpxIWE="
@@ -161,8 +161,8 @@ schema = 3
version = "v1.0.2"
hash = "sha256-+W9EIW7okXIXjWEgOaMh58eLvBZ7OshW2EhaIpNLSBU="
[mod."github.com/ollama/ollama"]
version = "v0.6.6"
hash = "sha256-a2Be14e+pcJo15fM/+0ksE9HVl8I4hW6ujqbpNh9bpA="
version = "v0.9.0"
hash = "sha256-r2eU+kMG3tuJy2B43RXsfmeltzM9t05NEmNiJAW5qr4="
[mod."github.com/otiai10/copy"]
version = "v1.14.1"
hash = "sha256-8RR7u17SbYg9AeBXVHIv5ZMU+kHmOcx0rLUKyz6YtU0="
@@ -182,14 +182,14 @@ schema = 3
version = "v1.0.0"
hash = "sha256-/FtmHnaGjdvEIKAJtrUfEhV7EVo5A/eYrtdnUkuxLDA="
[mod."github.com/samber/lo"]
version = "v1.49.1"
hash = "sha256-xMQS9Sx2Bpvwo/9JvSVkJ4RXYOSHm642WRqWA6y0AnU="
version = "v1.50.0"
hash = "sha256-KDFks82BKu39sGt0f972IyOkohV2U0r1YvsnlNLdugY="
[mod."github.com/sashabaranov/go-openai"]
version = "v1.38.2"
hash = "sha256-AnBycaxufzWlLS1YBq7MiHDED+Jqtu9oAySKcoL4HOA="
version = "v1.40.1"
hash = "sha256-GkToonIIF3GG+lwev1lJQ9rAAPJDjSaOkoXRC3OOlEA="
[mod."github.com/sergi/go-diff"]
version = "v1.3.2-0.20230802210424-5b0b94c5c0d3"
hash = "sha256-UcLU83CPMbSoKI8RLvLJ7nvGaE2xRSL1RjoHCVkMzUM="
version = "v1.4.0"
hash = "sha256-rs9NKpv/qcQEMRg7CmxGdP4HGuFdBxlpWf9LbA9wS4k="
[mod."github.com/skeema/knownhosts"]
version = "v1.3.1"
hash = "sha256-kjqQDzuncQNTuOYegqVZExwuOt/Z73m2ST7NZFEKixI="
@@ -212,8 +212,8 @@ schema = 3
version = "v0.15.1"
hash = "sha256-HLk6oUe7EoITrNvP0y8D6BtIgIcmDZYtb/xl/dufIoY="
[mod."github.com/ugorji/go/codec"]
version = "v1.2.12"
hash = "sha256-sp1LJ93UK7mFwgZqG8jxCgTCPgKR74HNU6XxX0Jfjm0="
version = "v1.2.14"
hash = "sha256-PoVXlCBE8SvMWpXx9FRsQOSAmE/+5SnPGr4m5BGoyIo="
[mod."github.com/xanzy/ssh-agent"]
version = "v0.3.3"
hash = "sha256-l3pGB6IdzcPA/HLk93sSN6NM2pKPy+bVOoacR5RC2+c="
@@ -221,56 +221,56 @@ schema = 3
version = "v1.1.0"
hash = "sha256-cA9qCCu8P1NSJRxgmpfkfa5rKyn9X+Y/9FSmSd5xjyo="
[mod."go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"]
version = "v0.60.0"
hash = "sha256-DkIpL4xUy+UIQBUK6VgbsI79TbZUltaIhXl4UJWym6E="
version = "v0.61.0"
hash = "sha256-o5w9k3VbqP3gaXI3Aelw93LLHH53U4PnkYVwc3MaY3Y="
[mod."go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"]
version = "v0.60.0"
hash = "sha256-twGSnNbXzcw5qvRiFc/zz5rS+nhmbgSVPcd5jrZjlDg="
version = "v0.61.0"
hash = "sha256-4pfXD7ErXhexSynXiEEQSAkWoPwHd7PEDE3M1Zi5gLM="
[mod."go.opentelemetry.io/otel"]
version = "v1.35.0"
hash = "sha256-LHrBtBnyDtvJGtrXHMPIFe7U53B4bZzpePB4u8Xo4Bg="
version = "v1.36.0"
hash = "sha256-j8wojdCtKal3LKojanHA8KXXQ0FkbWONpO8tUxpJDko="
[mod."go.opentelemetry.io/otel/metric"]
version = "v1.35.0"
hash = "sha256-K9I0LRZqSLrC09Cuk7tp0VEk3cUVDs8S5MGnu9jw92Q="
version = "v1.36.0"
hash = "sha256-z6Uqi4HhUljWIYd58svKK5MqcGbpcac+/M8JeTrUtJ8="
[mod."go.opentelemetry.io/otel/trace"]
version = "v1.35.0"
hash = "sha256-HC2+OGDe2rg0+E8WymQbUNoc249NXM1gIBJzK4UhcQE="
version = "v1.36.0"
hash = "sha256-owWD9x1lp8aIJqYt058BXPUsIMHdk3RI0escso0BxwA="
[mod."golang.org/x/arch"]
version = "v0.16.0"
hash = "sha256-+DMOuIw9GVyhM4VHdYCZepTU/EEHqDfrxJ2F83TOs5k="
version = "v0.18.0"
hash = "sha256-tUpUPERjmRi7zldj0oPlnbnBhEkcI9iQGvP1HqlsK10="
[mod."golang.org/x/crypto"]
version = "v0.37.0"
hash = "sha256-9NwDEcii1e2JYM/+3y1yNzWnt/ChMm27e9OtfuF39OM="
[mod."golang.org/x/net"]
version = "v0.39.0"
hash = "sha256-IP29+yGphWKUT7wHTyzqA2rnRT4AJ7oWcT6NKLzkWcM="
hash = "sha256-FtwjbVoAhZkx7F2hmzi9Y0J87CVVhWcrZzun+zWQLzc="
[mod."golang.org/x/net"]
version = "v0.41.0"
hash = "sha256-6/pi8rNmGvBFzkJQXkXkMfL1Bjydhg3BgAMYDyQ/Uvg="
[mod."golang.org/x/oauth2"]
version = "v0.29.0"
hash = "sha256-IzAypzW8cN5ZbQiIdMTcTiVuUNpMSkwuxeFrJZxcDl8="
version = "v0.30.0"
hash = "sha256-btD7BUtQpOswusZY5qIU90uDo38buVrQ0tmmQ8qNHDg="
[mod."golang.org/x/sync"]
version = "v0.13.0"
hash = "sha256-CElRNe74Or/ysUkb/m3Wcz/juO/tB5fhQbAaxA5AizY="
version = "v0.15.0"
hash = "sha256-Jf4ehm8H8YAWY6mM151RI5CbG7JcOFtmN0AZx4bE3UE="
[mod."golang.org/x/sys"]
version = "v0.32.0"
hash = "sha256-c9RRnyKQy9Kl8hpbtcgkm1O5H7gOdk9Rv925F8fZS6E="
version = "v0.33.0"
hash = "sha256-wlOzIOUgAiGAtdzhW/KPl/yUVSH/lvFZfs5XOuJ9LOQ="
[mod."golang.org/x/text"]
version = "v0.24.0"
hash = "sha256-qFbmteGOvJfvbLXiOSI8Fsz5Ixt2ZhSYx0/sIqApC7Y="
version = "v0.26.0"
hash = "sha256-N+27nBCyGvje0yCTlUzZoVZ0LRxx4AJ+eBlrFQVRlFQ="
[mod."golang.org/x/time"]
version = "v0.11.0"
hash = "sha256-ImTej/e5iUHbWPZMA4M2GYbsbiiZQxIrgcnYsc7uD68="
version = "v0.12.0"
hash = "sha256-Cp3oxrCMH2wyxjzr5SHVmyhgaoUuSl56Uy00Q7DYEpw="
[mod."google.golang.org/api"]
version = "v0.230.0"
hash = "sha256-ihEdZnRbQdwpbgj9AZEZLNY14FqHmacFGFocOqExSVY="
version = "v0.236.0"
hash = "sha256-tP1RSUSnQ4a0axgZQwEZgKF1E13nL02FSP1NPSZr0Rc="
[mod."google.golang.org/genproto/googleapis/api"]
version = "v0.0.0-20250422160041-2d3770c4ea7f"
hash = "sha256-Y4wbEHh9Un0QKplTl2S5lhWDUha9QThx5DhWJbDG9fo="
version = "v0.0.0-20250603155806-513f23925822"
hash = "sha256-0CS432v9zVhkVLqFpZtxBX8rvVqP67lb7qQ3es7RqIU="
[mod."google.golang.org/genproto/googleapis/rpc"]
version = "v0.0.0-20250422160041-2d3770c4ea7f"
version = "v0.0.0-20250603155806-513f23925822"
hash = "sha256-WK7iDtAhH19NPe3TywTQlGjDawNaDKWnxhFL9PgVUwM="
[mod."google.golang.org/grpc"]
version = "v1.72.0"
hash = "sha256-tqu+ACMfKjhqdCGN3jLEmtaHB5ywgHGaS/eDeDRnf+M="
version = "v1.73.0"
hash = "sha256-LfVlwip++q2DX70RU6CxoXglx1+r5l48DwlFD05G11c="
[mod."google.golang.org/protobuf"]
version = "v1.36.6"
hash = "sha256-lT5qnefI5FDJnowz9PEkAGylH3+fE+A3DJDkAyy9RMc="

View File

@@ -1 +1 @@
"1.4.196"
"1.4.200"

View File

@@ -0,0 +1,24 @@
# IDENTITY and PURPOSE
You are an expert Terraform plan analyser. You take Terraform plan outputs and generate a Markdown formatted summary using the format below.
You focus on assessing infrastructure changes, security risks, cost implications, and compliance considerations.
## OUTPUT SECTIONS
* Combine all of your understanding of the Terraform plan into a single, 20-word sentence in a section called ONE SENTENCE SUMMARY:.
* Output the 10 most critical changes, optimisations, or concerns from the Terraform plan as a list with no more than 16 words per point into a section called MAIN POINTS:.
* Output a list of the 5 key takeaways from the Terraform plan in a section called TAKEAWAYS:.
## OUTPUT INSTRUCTIONS
* Create the output using the formatting above.
* You only output human-readable Markdown.
* Output numbered lists, not bullets.
* Do not output warnings or notes—just the requested sections.
* Do not repeat items in the output sections.
* Do not start items with the same opening words.
## INPUT
INPUT:

View File

@@ -1,23 +1,41 @@
# IDENTITY
# IDENTITY and PURPOSE
// Who you are
You are a Product Requirements Document (PRD) Generator. Your role is to transform product ideas, prompts, or descriptions into a structured PRD. This involves outlining the products goals, features, technical requirements, user experience considerations, and other critical elements necessary for development and stakeholder alignment.
You create precise and accurate PRDs from the input you receive.
Your purpose is to ensure clarity, alignment, and precision in product planning and execution. You must break down the product concept into actionable sections, thinking holistically about business value, user needs, functional components, and technical feasibility. Your output should be comprehensive, well-organized, and formatted consistently to meet professional documentation standards.
# GOAL
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
// What we are trying to achieve
## STEPS
1. Create a great PRD.
* Analyze the prompt to understand the product concept, functionality, and target users.
# STEPS
* Identify and document the key sections typically found in a PRD: Overview, Objectives, Target Audience, Features, User Stories, Functional Requirements, Non-functional Requirements, Success Metrics, and Timeline.
- Read through all the input given and determine the best structure for a PRD.
* Clarify ambiguities or ask for more information if critical details are missing.
# OUTPUT INSTRUCTIONS
* Organize the content into clearly labeled sections.
- Create the PRD in Markdown.
* Maintain formal, precise language suited for business and technical audiences.
# INPUT
* Ensure each requirement is specific, testable, and unambiguous.
* Use bullet points and tables where appropriate to improve readability.
## OUTPUT INSTRUCTIONS
* The only output format should be Markdown.
* All content should be structured into clearly labeled PRD sections.
* Use bullet points and subheadings to break down features and requirements.
* Highlight priorities or MVP features where relevant.
* Include mock data or placeholders if actual data is not provided.
* Ensure you follow ALL these instructions when creating your output.
## INPUT
INPUT:

View File

@@ -3,9 +3,10 @@ package anthropic
import (
"context"
"fmt"
"github.com/samber/lo"
"strings"
"github.com/samber/lo"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
"github.com/danielmiessler/fabric/common"

View File

@@ -33,16 +33,20 @@ func NewClient() (ret *Client) {
ret.ApiUrl.Value = defaultBaseUrl
ret.ApiKey = ret.PluginBase.AddSetupQuestion("API key", false)
ret.ApiKey.Value = ""
ret.ApiHttpTimeout = ret.AddSetupQuestionCustom("HTTP Timeout", true,
"Specify HTTP timeout duration for Ollama requests (e.g. 30s, 5m, 1h)")
ret.ApiHttpTimeout.Value = "20m"
return
}
type Client struct {
*plugins.PluginBase
ApiUrl *plugins.SetupQuestion
ApiKey *plugins.SetupQuestion
apiUrl *url.URL
client *ollamaapi.Client
ApiUrl *plugins.SetupQuestion
ApiKey *plugins.SetupQuestion
apiUrl *url.URL
client *ollamaapi.Client
ApiHttpTimeout *plugins.SetupQuestion
}
type transport_sec struct {
@@ -63,7 +67,19 @@ func (o *Client) configure() (err error) {
return
}
o.client = ollamaapi.NewClient(o.apiUrl, &http.Client{Timeout: 1200000 * time.Millisecond, Transport: &transport_sec{underlyingTransport: http.DefaultTransport, ApiKey: o.ApiKey}})
timeout := 20 * time.Minute // Default timeout
if o.ApiHttpTimeout != nil {
parsed, err := time.ParseDuration(o.ApiHttpTimeout.Value)
if err == nil && o.ApiHttpTimeout.Value != "" {
timeout = parsed
} else if o.ApiHttpTimeout.Value != "" {
fmt.Printf("Invalid HTTP timeout format (%q), using default (20m): %v\n", o.ApiHttpTimeout.Value, err)
}
}
o.client = ollamaapi.NewClient(o.apiUrl, &http.Client{Timeout: timeout, Transport: &transport_sec{underlyingTransport: http.DefaultTransport, ApiKey: o.ApiKey}})
return
}

View File

@@ -1,20 +1,23 @@
// Package youtube provides YouTube video transcript and comment extraction functionality.
// This implementation relies on yt-dlp for reliable transcript extraction, which must be
// installed separately. The old YouTube API scraping methods have been removed due to
// YouTube's frequent changes and rate limiting.
package youtube
import (
"bytes"
"context"
"encoding/csv"
"encoding/json"
"flag"
"fmt"
"log"
"net/url"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/anaskhan96/soup"
"github.com/danielmiessler/fabric/plugins"
"google.golang.org/api/option"
"google.golang.org/api/youtube/v3"
@@ -94,112 +97,253 @@ func (o *YouTube) GrabTranscriptForUrl(url string, language string) (ret string,
}
func (o *YouTube) GrabTranscript(videoId string, language string) (ret string, err error) {
var transcript string
if transcript, err = o.GrabTranscriptBase(videoId, language); err != nil {
err = fmt.Errorf("transcript not available. (%v)", err)
return
}
// Parse the XML transcript
doc := soup.HTMLParse(transcript)
// Extract the text content from the <text> tags
textTags := doc.FindAll("text")
var textBuilder strings.Builder
for _, textTag := range textTags {
textBuilder.WriteString(strings.ReplaceAll(textTag.Text(), "&#39;", "'"))
textBuilder.WriteString(" ")
ret = textBuilder.String()
}
return
// Use yt-dlp for reliable transcript extraction
return o.tryMethodYtDlp(videoId, language)
}
func (o *YouTube) GrabTranscriptWithTimestamps(videoId string, language string) (ret string, err error) {
var transcript string
if transcript, err = o.GrabTranscriptBase(videoId, language); err != nil {
err = fmt.Errorf("transcript not available. (%v)", err)
// Use yt-dlp for reliable transcript extraction with timestamps
return o.tryMethodYtDlpWithTimestamps(videoId, language)
}
func (o *YouTube) tryMethodYtDlp(videoId string, language string) (ret string, err error) {
// Check if yt-dlp is available
if _, err = exec.LookPath("yt-dlp"); err != nil {
err = fmt.Errorf("yt-dlp not found in PATH. Please install yt-dlp to use YouTube transcript functionality")
return
}
// Parse the XML transcript
doc := soup.HTMLParse(transcript)
// Extract the text content from the <text> tags
textTags := doc.FindAll("text")
var textBuilder strings.Builder
for _, textTag := range textTags {
// Extract the start and duration attributes
start := textTag.Attrs()["start"]
dur := textTag.Attrs()["dur"]
end := fmt.Sprintf("%f", parseFloat(start)+parseFloat(dur))
// Format the timestamps
startFormatted := formatTimestamp(parseFloat(start))
endFormatted := formatTimestamp(parseFloat(end))
text := strings.ReplaceAll(textTag.Text(), "&#39;", "'")
textBuilder.WriteString(fmt.Sprintf("[%s - %s] %s\n", startFormatted, endFormatted, text))
// Create a temporary directory for yt-dlp output
tempDir := "/tmp/fabric-youtube-" + videoId
if err = os.MkdirAll(tempDir, 0755); err != nil {
err = fmt.Errorf("failed to create temp directory: %v", err)
return
}
defer os.RemoveAll(tempDir)
// Use yt-dlp to get transcript
videoURL := "https://www.youtube.com/watch?v=" + videoId
cmd := exec.Command("yt-dlp",
"--write-auto-subs",
"--sub-lang", language,
"--skip-download",
"--sub-format", "vtt",
"--quiet",
"--no-warnings",
"-o", tempDir+"/%(title)s.%(ext)s",
videoURL)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err = cmd.Run(); err != nil {
err = fmt.Errorf("yt-dlp failed: %v, stderr: %s", err, stderr.String())
return
}
// Find the VTT file in the temp directory
cmd2 := exec.Command("find", tempDir, "-name", "*."+language+".vtt", "-type", "f")
var findOut bytes.Buffer
cmd2.Stdout = &findOut
if err = cmd2.Run(); err != nil {
err = fmt.Errorf("failed to find VTT file: %v", err)
return
}
vttFiles := strings.Split(strings.TrimSpace(findOut.String()), "\n")
if len(vttFiles) == 0 || vttFiles[0] == "" {
// Try without language suffix
cmd3 := exec.Command("find", tempDir, "-name", "*.vtt", "-type", "f")
cmd3.Stdout = &findOut
findOut.Reset()
if err = cmd3.Run(); err != nil {
err = fmt.Errorf("no VTT files found")
return
}
vttFiles = strings.Split(strings.TrimSpace(findOut.String()), "\n")
if len(vttFiles) == 0 || vttFiles[0] == "" {
err = fmt.Errorf("no VTT files found in temp directory")
return
}
}
return o.readAndCleanVTTFile(vttFiles[0])
}
func (o *YouTube) tryMethodYtDlpWithTimestamps(videoId string, language string) (ret string, err error) {
// Check if yt-dlp is available
if _, err = exec.LookPath("yt-dlp"); err != nil {
err = fmt.Errorf("yt-dlp not found in PATH. Please install yt-dlp to use YouTube transcript functionality")
return
}
// Create a temporary directory for yt-dlp output
tempDir := "/tmp/fabric-youtube-" + videoId
if err = os.MkdirAll(tempDir, 0755); err != nil {
err = fmt.Errorf("failed to create temp directory: %v", err)
return
}
defer os.RemoveAll(tempDir)
// Use yt-dlp to get transcript
videoURL := "https://www.youtube.com/watch?v=" + videoId
cmd := exec.Command("yt-dlp",
"--write-auto-subs",
"--sub-lang", language,
"--skip-download",
"--sub-format", "vtt",
"--quiet",
"--no-warnings",
"-o", tempDir+"/%(title)s.%(ext)s",
videoURL)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err = cmd.Run(); err != nil {
err = fmt.Errorf("yt-dlp failed: %v, stderr: %s", err, stderr.String())
return
}
// Find the VTT file in the temp directory
cmd2 := exec.Command("find", tempDir, "-name", "*."+language+".vtt", "-type", "f")
var findOut bytes.Buffer
cmd2.Stdout = &findOut
if err = cmd2.Run(); err != nil {
err = fmt.Errorf("failed to find VTT file: %v", err)
return
}
vttFiles := strings.Split(strings.TrimSpace(findOut.String()), "\n")
if len(vttFiles) == 0 || vttFiles[0] == "" {
// Try without language suffix
cmd3 := exec.Command("find", tempDir, "-name", "*.vtt", "-type", "f")
cmd3.Stdout = &findOut
findOut.Reset()
if err = cmd3.Run(); err != nil {
err = fmt.Errorf("no VTT files found")
return
}
vttFiles = strings.Split(strings.TrimSpace(findOut.String()), "\n")
if len(vttFiles) == 0 || vttFiles[0] == "" {
err = fmt.Errorf("no VTT files found in temp directory")
return
}
}
return o.readAndFormatVTTWithTimestamps(vttFiles[0])
}
func (o *YouTube) readAndCleanVTTFile(filename string) (ret string, err error) {
var content []byte
if content, err = os.ReadFile(filename); err != nil {
return
}
// Convert VTT to plain text
lines := strings.Split(string(content), "\n")
var textBuilder strings.Builder
for _, line := range lines {
line = strings.TrimSpace(line)
// Skip WEBVTT header, timestamps, and empty lines
if line == "" || line == "WEBVTT" || strings.Contains(line, "-->") ||
strings.HasPrefix(line, "NOTE") || strings.HasPrefix(line, "STYLE") ||
strings.HasPrefix(line, "Kind:") || strings.HasPrefix(line, "Language:") ||
isTimeStamp(line) {
continue
}
// Remove VTT formatting tags
line = removeVTTTags(line)
if line != "" {
textBuilder.WriteString(line)
textBuilder.WriteString(" ")
}
}
ret = strings.TrimSpace(textBuilder.String())
if ret == "" {
err = fmt.Errorf("no transcript content found in VTT file")
}
ret = textBuilder.String()
return
}
func parseFloat(s string) float64 {
f, _ := strconv.ParseFloat(s, 64)
return f
}
func formatTimestamp(seconds float64) string {
hours := int(seconds) / 3600
minutes := (int(seconds) % 3600) / 60
secs := int(seconds) % 60
return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, secs)
}
func (o *YouTube) GrabTranscriptBase(videoId string, language string) (ret string, err error) {
if err = o.initService(); err != nil {
func (o *YouTube) readAndFormatVTTWithTimestamps(filename string) (ret string, err error) {
var content []byte
if content, err = os.ReadFile(filename); err != nil {
return
}
watchUrl := "https://www.youtube.com/watch?v=" + videoId
var resp string
if resp, err = soup.Get(watchUrl); err != nil {
return
}
// Parse VTT and preserve timestamps
lines := strings.Split(string(content), "\n")
var textBuilder strings.Builder
var currentTimestamp string
doc := soup.HTMLParse(resp)
scriptTags := doc.FindAll("script")
for _, scriptTag := range scriptTags {
if strings.Contains(scriptTag.Text(), "captionTracks") {
regex := regexp.MustCompile(`"captionTracks":(\[.*?\])`)
match := regex.FindStringSubmatch(scriptTag.Text())
if len(match) > 1 {
var captionTracks []struct {
BaseURL string `json:"baseUrl"`
}
for _, line := range lines {
line = strings.TrimSpace(line)
if err = json.Unmarshal([]byte(match[1]), &captionTracks); err != nil {
return
}
// Skip WEBVTT header and empty lines
if line == "" || line == "WEBVTT" || strings.HasPrefix(line, "NOTE") ||
strings.HasPrefix(line, "STYLE") || strings.HasPrefix(line, "Kind:") ||
strings.HasPrefix(line, "Language:") {
continue
}
if len(captionTracks) > 0 {
transcriptURL := captionTracks[0].BaseURL
for _, captionTrack := range captionTracks {
parsedUrl, error := url.Parse(captionTrack.BaseURL)
if error != nil {
err = fmt.Errorf("error parsing caption track")
}
parsedUrlParams, _ := url.ParseQuery(parsedUrl.RawQuery)
if parsedUrlParams["lang"][0] == language {
transcriptURL = captionTrack.BaseURL
}
}
ret, err = soup.Get(transcriptURL)
return
}
// Check if this line is a timestamp
if strings.Contains(line, "-->") {
// Extract start time for this segment
parts := strings.Split(line, " --> ")
if len(parts) >= 1 {
currentTimestamp = formatVTTTimestamp(parts[0])
}
continue
}
// Skip numeric sequence identifiers
if isTimeStamp(line) && !strings.Contains(line, ":") {
continue
}
// This should be transcript text
if line != "" {
// Remove VTT formatting tags
cleanText := removeVTTTags(line)
if cleanText != "" && currentTimestamp != "" {
textBuilder.WriteString(fmt.Sprintf("[%s] %s\n", currentTimestamp, cleanText))
}
}
}
err = fmt.Errorf("transcript not found")
ret = strings.TrimSpace(textBuilder.String())
if ret == "" {
err = fmt.Errorf("no transcript content found in VTT file")
}
return
}
func formatVTTTimestamp(vttTime string) string {
// VTT timestamps are in format "00:00:01.234" - convert to "00:00:01"
parts := strings.Split(vttTime, ".")
if len(parts) > 0 {
return parts[0]
}
return vttTime
}
func isTimeStamp(s string) bool {
// Match timestamps like "00:00:01.234" or just numbers
timestampRegex := regexp.MustCompile(`^\d+$|^\d{2}:\d{2}:\d{2}`)
return timestampRegex.MatchString(s)
}
func removeVTTTags(s string) string {
// Remove VTT tags like <c.colorE5E5E5>, </c>, etc.
tagRegex := regexp.MustCompile(`<[^>]*>`)
return tagRegex.ReplaceAllString(s, "")
}
func (o *YouTube) GrabComments(videoId string) (ret []string, err error) {
if err = o.initService(); err != nil {
return

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.196"
var version = "v1.4.200"