merge: demo wiring — tapir auth/run + config (Worker F, agent/demo-wiring)
# Conflicts: # cmd/tapir/main.go
This commit is contained in:
+105
-9
@@ -1,12 +1,33 @@
|
||||
// Command tapir is the service entrypoint and CLI. Subcommands are dispatched off
|
||||
// os.Args[1]; each lives in its own file (list.go, show.go, …). The switch is kept
|
||||
// deliberately flat so concurrently-added subcommands union cleanly.
|
||||
// Command tapir is the service entrypoint and CLI. Subcommands are dispatched
|
||||
// off os.Args[1]; each lives in its own file (list.go, show.go, …). It serves
|
||||
// the Stage-0 demo:
|
||||
//
|
||||
// tapir auth mint a YouTube refresh token interactively (one-time setup)
|
||||
// tapir run detect new videos, summarize them, deliver to the store
|
||||
// tapir list list stored summaries, recent first
|
||||
// tapir show show one stored summary in full
|
||||
//
|
||||
// Standalone-vs-homelab is a wiring choice (ADR-003): every dependency is
|
||||
// resolved from config (internal/config) and the SecretStore port, so live
|
||||
// credentials plug in at runtime without code changes.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"gitea.d-ma.be/mathias/tapir/internal/adapters/llm"
|
||||
"gitea.d-ma.be/mathias/tapir/internal/adapters/secrets"
|
||||
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
|
||||
"gitea.d-ma.be/mathias/tapir/internal/adapters/summarizer"
|
||||
"gitea.d-ma.be/mathias/tapir/internal/adapters/youtube"
|
||||
"gitea.d-ma.be/mathias/tapir/internal/auth"
|
||||
"gitea.d-ma.be/mathias/tapir/internal/config"
|
||||
"gitea.d-ma.be/mathias/tapir/internal/runner"
|
||||
"gitea.d-ma.be/mathias/tapir/internal/usecase"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -15,27 +36,102 @@ func main() {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
var err error
|
||||
switch os.Args[1] {
|
||||
case "list":
|
||||
err = runList(ctx, os.Args[2:])
|
||||
case "show":
|
||||
err = runShow(ctx, os.Args[2:])
|
||||
case "auth":
|
||||
err = cmdAuth(ctx, log)
|
||||
case "run":
|
||||
err = cmdRun(ctx, log)
|
||||
default:
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "tapir:", err)
|
||||
log.Error("command failed", "command", os.Args[1], "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: tapir <command> [args]")
|
||||
fmt.Fprintln(os.Stderr, "commands:")
|
||||
fmt.Fprintln(os.Stderr, " list [-limit N] list stored summaries, recent first")
|
||||
fmt.Fprintln(os.Stderr, " show <video-id> show one summary in full")
|
||||
fmt.Fprint(os.Stderr, `tapir — summarize new videos from your YouTube subscriptions
|
||||
|
||||
usage:
|
||||
tapir auth one-time: authorize YouTube and store a refresh token
|
||||
tapir run detect new videos, summarize, deliver to your store
|
||||
tapir list [-limit N] list stored summaries, recent first
|
||||
tapir show <video-id> show one summary in full
|
||||
|
||||
configuration is via TAPIR_* environment variables (see .env.example).
|
||||
`)
|
||||
}
|
||||
|
||||
// cmdAuth runs the interactive OAuth flow and persists the refresh token.
|
||||
func cmdAuth(ctx context.Context, log *slog.Logger) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cfg.ValidateForAuth(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secretStore := secrets.NewFileStore(cfg.SecretsFile)
|
||||
authCfg := auth.Config{
|
||||
ClientID: cfg.YTClientID,
|
||||
ClientSecret: cfg.YTClientSecret,
|
||||
RedirectURL: "http://" + cfg.OAuthRedirectAddr + "/callback",
|
||||
TokenRef: cfg.YTTokenRef,
|
||||
}
|
||||
log.Info("starting youtube authorization", "redirect", authCfg.RedirectURL, "token_ref", cfg.YTTokenRef)
|
||||
return auth.Run(ctx, authCfg, secretStore, os.Stdout)
|
||||
}
|
||||
|
||||
// cmdRun wires the adapters and engine, then runs the watch→summarize→deliver
|
||||
// loop. With TAPIR_POLL_INTERVAL unset it performs a single pass.
|
||||
func cmdRun(ctx context.Context, log *slog.Logger) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cfg.ValidateForRun(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
st, err := store.New(ctx, cfg.DBDSN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
secretStore := secrets.NewFileStore(cfg.SecretsFile)
|
||||
src := youtube.New(youtube.Config{
|
||||
ClientID: cfg.YTClientID,
|
||||
ClientSecret: cfg.YTClientSecret,
|
||||
TokenSecretRef: cfg.YTTokenRef,
|
||||
PreferredLanguages: []string{"en"},
|
||||
}, secretStore)
|
||||
|
||||
// Local Primary only; no BYO fallback for the demo (fallback nil).
|
||||
primary := summarizer.Endpoint{
|
||||
Client: llm.New(cfg.GatewayURL, cfg.GatewayKey, cfg.SummarizerModel, cfg.SummarizerTimeout),
|
||||
Provider: "local",
|
||||
Model: cfg.SummarizerModel,
|
||||
}
|
||||
sum := summarizer.New(primary, nil)
|
||||
|
||||
engine := usecase.NewEngine(src, sum, st)
|
||||
r := runner.New(src, st, engine, cfg.UserID, log)
|
||||
|
||||
log.Info("starting run", "user", cfg.UserID, "model", cfg.SummarizerModel,
|
||||
"gateway", cfg.GatewayURL, "poll_interval", cfg.PollInterval)
|
||||
return r.Loop(ctx, cfg.PollInterval)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user