Files
Strix/.claude/skills/add_generate_strix/SKILL.md
T
eduard256 102d2aa225 Add add_generate_strix skill
Skill for registering new protocol extractors with the Strix config
generator. Narrow in scope: adds ONE extractor + tests per protocol,
never touches pkg/generate/ internals. References xiaomi as the golden
reference for both the extractor pattern and the 16 mandatory test
scenarios.
2026-04-18 12:24:26 +00:00

9.8 KiB

name, description, disable-model-invocation, argument-hint
name description disable-model-invocation argument-hint
add_generate_strix Register a new protocol extractor for the Strix config generator. Use when adding support for Tuya, Tapo, Nest, Ring, Roborock and other camera protocols that need credentials in a separate YAML section of frigate-config.yaml. This skill adds the glue; it does NOT add the protocol itself -- that's /add_protocol_strix. true
protocol-name

Add Generate Extractor to Strix

You are registering a new protocol with the Strix config generator so that /api/generate can produce a valid frigate-config.yaml for cameras of that protocol.

The protocol name is provided as argument (e.g. /add_generate_strix tuya). If no argument, use AskUserQuestion to ask which protocol.

This skill is NARROW. It adds ONE extractor function plus tests. Do NOT modify pkg/generate/ internals (registry.go, config.go, writer.go, insert.go) -- they are protocol-agnostic by design. If you are tempted to change them, stop and ask the user.

Repositories

  • Strix: current working directory (/home/user/Strix)
  • go2rtc: /home/user/go2rtc (reference for YAML section format)

STEP 0: Study the xiaomi reference

Before writing anything, read these files completely. Xiaomi is the golden reference for this skill. Every new protocol copies its structure.

  • internal/xiaomi/xiaomi.go -- how extractForConfig is written and where generate.RegisterExtract is called inside Init()
  • pkg/generate/xiaomi_test.go -- the 16 test scenarios that every new protocol must pass
  • pkg/generate/registry.go -- the ExtractFunc contract (do not modify, only understand)

Then glance at these to understand what NOT to touch:

  • pkg/generate/config.go -- how runExtract is called for main/sub/go2rtc-override URLs
  • pkg/generate/writer.go -- how writeCredentials nests sections under go2rtc:
  • pkg/generate/insert.go -- upsertCredentials merges into existing configs

STEP 1: Study the protocol in go2rtc

Read these files in the go2rtc repo:

  • internal/<proto>/README.md -- authoritative YAML format
  • internal/<proto>/<proto>.go -- how go2rtc unmarshals the credentials section (look for yaml:"<proto>" in a struct tag)
  • pkg/<proto>/ -- URL scheme: what goes into userinfo, what into query, what into path

You need to answer exactly three questions:

  1. Where does the secret live in the URL?

    • ?token=X in query (xiaomi, nest, ring)
    • userinfo password (tapo, roborock)
    • Custom (read the module)
  2. What is the YAML section name? Usually the same as the URL scheme (tuya:, tapo:). Confirm from go2rtc's yaml: struct tag.

  3. What is the YAML key format? Examples:

    • xiaomi: "<userID>" (quoted, matches ci field in device)
    • tapo: <user>@<host>
    • roborock: <username>
    • nest: "<userID>"

Write these three answers down. They drive the extractor.


STEP 2: Verify the internal module already exists

Check internal/<proto>/<proto>.go exists and has a working Init() with tester.RegisterSource or similar. If the module does NOT exist, stop and tell the user to run /add_protocol_strix <proto> first. This skill only adds the generate hook to an existing module.


STEP 3: Add the extractor to internal/<proto>/<proto>.go

Open internal/<proto>/<proto>.go.

3a. Add import (if not present):

import "github.com/eduard256/strix/pkg/generate"

3b. Register the extractor at the END of Init():

generate.RegisterExtract("<proto>", extractForConfig)

3c. Add the function. Use the exact xiaomi style. Place it directly after Init().

Template: secret in query (xiaomi-like)

// extractForConfig strips ?<secret>=... from <proto>:// URL and returns
// <key> + <token> for a go2rtc:<section>: block.
// ex. <proto>://<user>:<region>@<ip>?...&<secret>=T
//   -> <proto>://<user>:<region>@<ip>?..., "<section>", "<user>", "T"
func extractForConfig(rawURL string) (cleaned, section, key, value string) {
	u, err := url.Parse(rawURL)
	if err != nil || u.User == nil {
		return rawURL, "", "", ""
	}

	q := u.Query()
	token := q.Get("<secret>")
	if token == "" {
		return rawURL, "", "", ""
	}
	q.Del("<secret>")
	u.RawQuery = q.Encode()

	return u.String(), "<section>", u.User.Username(), token
}

Template: secret in userinfo password (tapo-like)

func extractForConfig(rawURL string) (cleaned, section, key, value string) {
	u, err := url.Parse(rawURL)
	if err != nil || u.User == nil {
		return rawURL, "", "", ""
	}

	pw, ok := u.User.Password()
	if !ok || pw == "" {
		return rawURL, "", "", ""
	}

	// ex. tapo: key = "admin@192.168.1.100"
	key = u.User.Username() + "@" + u.Host
	// URL stays as-is -- go2rtc reads credentials from userinfo directly
	return rawURL, "<section>", key, pw
}

Rules for the extractor (strict)

  • MUST return (rawURL, "", "", "") on any parse error or missing secret. Never return an empty cleaned.
  • MUST NOT log, MUST NOT touch filesystem, MUST NOT call APIs.
  • MUST be deterministic -- same input always returns same output.
  • MUST NOT URL-encode the token value. writeCredentials emits it raw; YAML parses V1:abc+/= fine.
  • Keep it under 20 lines. If it grows, you're doing too much.

STEP 4: Write pkg/generate/<proto>_test.go

Copy the structure from pkg/generate/xiaomi_test.go exactly. Change:

  • registerXiaomi -> register<Proto> (use a new sync.Once per file -- do NOT share registerOnce across files)
  • xurl(...) -> <proto>url(...) -- builds a URL in this protocol's format
  • Test function names: TestXiaomi_* -> Test<Proto>_*
  • All string literals that reference xiaomi://, "acc1", V1:TOK_A -> equivalents for this protocol

The 16 tests to write (all MUST pass)

All scenarios from xiaomi_test.go are relevant and must be present:

# Test What it verifies
1 NewConfig_SingleCamera Nested go2rtc:\n <section>:\n <key>: <value> with correct indentation. URL in streams has no secret left.
2 SameAccount_TokenNotDuplicated Two cameras, same key -> exactly one entry in the section.
3 TwoAccounts_SortedKeys Two keys in the section appear in ASCII-sorted order.
4 TokenRefresh_OverwritesValue Re-adding a camera with a new token replaces the stored value, exactly one key remains.
5 MainAndSub_SameAccount_OneToken Main + Sub with identical credentials -> one key.
6 MainAndSub_DifferentAccounts Main + Sub with two accounts -> two keys.
7 Scale_10Cameras_3Accounts 10 cameras across 3 accounts sequentially added -> exactly 3 keys at the end, most-recent values.
8 URLWithoutToken_NoSection URL missing the secret -> no <section>: header written (check "\n <section>:\n", not the URL scheme substring).
9 MalformedURL_DoesNotPanic <proto>://%%%bad does not crash.
10 TokenSpecialChars_PreservedRaw Secret with +, /, =, : is emitted verbatim.
11 Go2RTCOverride_PassesThroughExtractor req.Go2RTC.MainStreamSource with this protocol is also extracted.
12 AddToConfig_NoExistingSection Start from rtsp-only config, add this protocol -> new <section>: block created under go2rtc:.
13 AddToConfig_ExistingSection Start from a config that already has <section>: -> new key merged, one header.
14 CustomName_URLStillClean req.Name set, URL still has no secret in streams.
15 MixedProtocols rtsp + this protocol together -- rtsp URL untouched, secret extracted.
16 SectionOrder Order: go2rtc -> streams -> <section> -> cameras -> version.

Common pitfalls in tests

  • assertNotContains(cfg, "<section>:") is WRONG when the URL scheme contains the same substring. Use "\n <section>:\n" instead (nested form) to avoid matching <proto>://.
  • For protocols where extractForConfig returns rawURL unchanged (userinfo template), the "URL cleaning" assertion assertNotContains(cfg, "token=") does not apply -- skip or adjust.
  • Use registerOnce with sync.Once so running the whole package test suite twice does not duplicate-register the extractor.

STEP 5: Build and run the tests

cd /home/user/Strix
go build ./...
go test ./pkg/generate/ -v -run Test<Proto>

Every test must pass. If any fails:

  • Re-read the corresponding xiaomi test and the difference in your version.
  • Re-read the extractor function -- usually the bug is there, not in generate internals.
  • Do NOT "fix" pkg/generate/ to make tests pass. The generator has 16 passing tests for xiaomi already -- your protocol must fit the same contract.

STEP 6: Sanity check the full generator output

Run once with a realistic URL manually (Bash + go run inline tool) and eyeball the YAML. Confirm:

  • go2rtc: is top-level
  • streams: and <section>: are both siblings under go2rtc: with 2-space indent
  • Keys under <section>: use 4-space indent
  • No duplicate headers
  • Secret appears verbatim (no %2F, no %3D)

Do NOT commit. Leave changes staged for the user to review and commit manually.


ABSOLUTES -- DO NOT VIOLATE

  1. Never modify pkg/generate/registry.go, config.go, writer.go, insert.go. If you think you need to, the protocol probably doesn't fit the extractor contract -- stop and discuss with the user.
  2. Never add a new extractor to pkg/generate/. Extractors live in internal/<proto>/, the test file is the only thing in pkg/generate/.
  3. Never modify pkg/<proto>/. That code comes from go2rtc; we don't own it.
  4. Never write tests in internal/<proto>/. All generator tests go to pkg/generate/<proto>_test.go.
  5. Never call generate.RegisterExtract from inside a test. Use sync.Once + a helper like register<Proto>() inside the test file.
  6. Never commit. Leave the changes for the user.
  7. Never skip tests. All 16 scenarios are mandatory -- they caught real regressions during development.