litclaude-ai 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/LICENSE +21 -0
  3. package/README.md +369 -0
  4. package/README_ko-KR.md +374 -0
  5. package/RELEASE_CHECKLIST.md +165 -0
  6. package/bin/litclaude-ai.js +643 -0
  7. package/cover.png +0 -0
  8. package/docs/agents.md +67 -0
  9. package/docs/hooks.md +134 -0
  10. package/docs/lsp.md +40 -0
  11. package/docs/migration.md +209 -0
  12. package/docs/workflow-compatibility-audit.md +119 -0
  13. package/generate_cover.py +123 -0
  14. package/package.json +48 -0
  15. package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
  16. package/plugins/litclaude/.lsp.json +13 -0
  17. package/plugins/litclaude/.mcp.json +9 -0
  18. package/plugins/litclaude/agents/boulder-executor.md +12 -0
  19. package/plugins/litclaude/agents/librarian-researcher.md +15 -0
  20. package/plugins/litclaude/agents/oracle-verifier.md +16 -0
  21. package/plugins/litclaude/agents/prometheus-planner.md +13 -0
  22. package/plugins/litclaude/agents/qa-runner.md +16 -0
  23. package/plugins/litclaude/agents/quality-reviewer.md +17 -0
  24. package/plugins/litclaude/bin/litclaude-hook.js +110 -0
  25. package/plugins/litclaude/bin/litclaude-hud.js +271 -0
  26. package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
  27. package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
  28. package/plugins/litclaude/commands/deep-interview.md +21 -0
  29. package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
  30. package/plugins/litclaude/commands/lit-loop.md +40 -0
  31. package/plugins/litclaude/commands/lit-plan.md +35 -0
  32. package/plugins/litclaude/commands/litgoal.md +30 -0
  33. package/plugins/litclaude/commands/review-work.md +35 -0
  34. package/plugins/litclaude/commands/start-work.md +36 -0
  35. package/plugins/litclaude/hooks/hooks.json +54 -0
  36. package/plugins/litclaude/lib/context-pressure.mjs +25 -0
  37. package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
  38. package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
  39. package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
  40. package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
  41. package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
  42. package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
  43. package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
  44. package/plugins/litclaude/lib/workflow-check.mjs +83 -0
  45. package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
  46. package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
  47. package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
  48. package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
  49. package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
  50. package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
  51. package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
  52. package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
  53. package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
  54. package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
  55. package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
  56. package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
  57. package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
  58. package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
  59. package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
  60. package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
  61. package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
  62. package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
  63. package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
  64. package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
  65. package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
  66. package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
  67. package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
  68. package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
  69. package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
  70. package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
  71. package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
  72. package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
  73. package/plugins/litclaude/skills/programming/SKILL.md +106 -0
  74. package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
  75. package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
  76. package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
  77. package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
  78. package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
  79. package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
  80. package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
  81. package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
  82. package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
  83. package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
  84. package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
  85. package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
  86. package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
  87. package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
  88. package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
  89. package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
  90. package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
  91. package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
  92. package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
  93. package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
  94. package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
  95. package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
  96. package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
  97. package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
  98. package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
  99. package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
  100. package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
  101. package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
  102. package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
  103. package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
  104. package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
  105. package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
  106. package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
  107. package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
  108. package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
  109. package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
  110. package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
  111. package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
  112. package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
  113. package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
  114. package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
  115. package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
  116. package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
  117. package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
  118. package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
  119. package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
  120. package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
  121. package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
  122. package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
  123. package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
  124. package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
  125. package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
  126. package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
  127. package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
  128. package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
  129. package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
  130. package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
  131. package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
  132. package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
  133. package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
  134. package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
  135. package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
  136. package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
  137. package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
  138. package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
  139. package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
  140. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
  141. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
  142. package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
  143. package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
  144. package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
  145. package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
  146. package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
  147. package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
  148. package/plugins/litclaude/skills/rules/SKILL.md +66 -0
  149. package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
  150. package/scripts/audit-plan-checkboxes.mjs +37 -0
  151. package/scripts/doctor.mjs +41 -0
  152. package/scripts/inspect-agent-tools.mjs +27 -0
  153. package/scripts/postinstall.mjs +50 -0
  154. package/scripts/qa-claude-plugin-smoke.sh +60 -0
  155. package/scripts/qa-portable-install.sh +136 -0
  156. package/scripts/validate-plugin.mjs +72 -0
@@ -0,0 +1,641 @@
1
+ # HTTP Backend Stack — gin + slog + validator + pgx
2
+
3
+ The canonical production HTTP service skeleton. Distilled from the [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) codebase — a real proxy serving OpenAI / Gemini / Claude / Codex APIs in production, with SSE streaming, WebSocket upgrades, request logging, and hot-reload config.
4
+
5
+ If you are tempted to pick echo or chi instead, see `libraries.md` — gin wins on ecosystem, not technical merit, and the win is large enough to matter.
6
+
7
+ ---
8
+
9
+ ## `go.mod`
10
+
11
+ ```go
12
+ module github.com/your-org/myservice
13
+
14
+ go 1.23
15
+
16
+ require (
17
+ github.com/gin-gonic/gin v1.10.1
18
+ github.com/go-playground/validator/v10 v10.22.1
19
+ github.com/caarlos0/env/v11 v11.2.2
20
+ github.com/google/uuid v1.6.0
21
+ github.com/jackc/pgx/v5 v5.7.6
22
+ golang.org/x/sync v0.18.0
23
+ )
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Project structure
29
+
30
+ ```
31
+ cmd/server/main.go # ≤ 50 LOC; flags → run.Execute(ctx)
32
+ internal/
33
+ cmd/run.go # ~150 LOC; signal handling, config load, server.Run
34
+ config/config.go # env-driven Config struct
35
+ api/
36
+ server.go # gin.Engine setup, route mounting, http.Server
37
+ middleware/
38
+ request_id.go
39
+ request_logging.go
40
+ auth.go
41
+ recovery.go
42
+ cors.go
43
+ handlers/
44
+ users.go # one file per resource
45
+ streams.go # SSE / WebSocket endpoints
46
+ domain/ # smart-constructor types (Email, UserID, ...)
47
+ service/ # business logic
48
+ store/ # pgx + sqlc
49
+ obs/
50
+ logger.go # slog setup
51
+ ```
52
+
53
+ ---
54
+
55
+ ## `cmd/server/main.go`
56
+
57
+ ```go
58
+ package main
59
+
60
+ import (
61
+ "context"
62
+ "log/slog"
63
+ "os"
64
+ "os/signal"
65
+ "syscall"
66
+
67
+ "github.com/your-org/myservice/internal/cmd"
68
+ )
69
+
70
+ func main() {
71
+ ctx, stop := signal.NotifyContext(context.Background(),
72
+ syscall.SIGINT, syscall.SIGTERM)
73
+ defer stop()
74
+
75
+ if err := cmd.Execute(ctx); err != nil {
76
+ slog.Error("fatal", slog.Any("err", err))
77
+ os.Exit(1)
78
+ }
79
+ }
80
+ ```
81
+
82
+ That is the entire `main`. Anything more is a smell.
83
+
84
+ ---
85
+
86
+ ## `internal/config/config.go`
87
+
88
+ ```go
89
+ package config
90
+
91
+ import (
92
+ "time"
93
+ "github.com/caarlos0/env/v11"
94
+ )
95
+
96
+ type Config struct {
97
+ Host string `env:"HOST" envDefault:"0.0.0.0"`
98
+ Port int `env:"PORT" envDefault:"8080"`
99
+ DatabaseURL string `env:"DATABASE_URL,required"`
100
+ ReadTimeout time.Duration `env:"READ_TIMEOUT" envDefault:"15s"`
101
+ WriteTimeout time.Duration `env:"WRITE_TIMEOUT" envDefault:"30s"`
102
+ ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" envDefault:"20s"`
103
+ LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
104
+ LogFormat string `env:"LOG_FORMAT" envDefault:"json"`
105
+ Env string `env:"ENV" envDefault:"development"`
106
+ }
107
+
108
+ func Load() (Config, error) {
109
+ var cfg Config
110
+ if err := env.Parse(&cfg); err != nil {
111
+ return Config{}, err
112
+ }
113
+ return cfg, nil
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## `internal/obs/logger.go`
120
+
121
+ ```go
122
+ package obs
123
+
124
+ import (
125
+ "context"
126
+ "log/slog"
127
+ "os"
128
+ )
129
+
130
+ type ctxKey struct{ name string }
131
+ var requestIDKey = ctxKey{"request_id"}
132
+
133
+ func NewLogger(level, format string) *slog.Logger {
134
+ var lvl slog.Level
135
+ _ = lvl.UnmarshalText([]byte(level))
136
+
137
+ opts := &slog.HandlerOptions{Level: lvl, AddSource: true}
138
+
139
+ var h slog.Handler
140
+ switch format {
141
+ case "text":
142
+ h = slog.NewTextHandler(os.Stdout, opts)
143
+ default:
144
+ h = slog.NewJSONHandler(os.Stdout, opts)
145
+ }
146
+ return slog.New(&ctxHandler{Handler: h})
147
+ }
148
+
149
+ // ctxHandler pulls request_id from ctx into every log line.
150
+ type ctxHandler struct{ slog.Handler }
151
+
152
+ func (h *ctxHandler) Handle(ctx context.Context, r slog.Record) error {
153
+ if id, ok := ctx.Value(requestIDKey).(string); ok && id != "" {
154
+ r.AddAttrs(slog.String("request_id", id))
155
+ }
156
+ return h.Handler.Handle(ctx, r)
157
+ }
158
+
159
+ func WithRequestID(ctx context.Context, id string) context.Context {
160
+ return context.WithValue(ctx, requestIDKey, id)
161
+ }
162
+ ```
163
+
164
+ ---
165
+
166
+ ## `internal/api/server.go`
167
+
168
+ ```go
169
+ package api
170
+
171
+ import (
172
+ "context"
173
+ "fmt"
174
+ "log/slog"
175
+ "net/http"
176
+
177
+ "github.com/gin-gonic/gin"
178
+ "github.com/your-org/myservice/internal/api/handlers"
179
+ "github.com/your-org/myservice/internal/api/middleware"
180
+ "github.com/your-org/myservice/internal/config"
181
+ )
182
+
183
+ type Server struct {
184
+ cfg config.Config
185
+ srv *http.Server
186
+ logger *slog.Logger
187
+ }
188
+
189
+ func New(cfg config.Config, logger *slog.Logger, h *handlers.Handler) *Server {
190
+ gin.SetMode(gin.ReleaseMode)
191
+ r := gin.New()
192
+
193
+ // Middleware order matters — see "Middleware ordering" below.
194
+ r.Use(
195
+ middleware.RequestID(), // 1. assign request_id first
196
+ middleware.Recovery(logger), // 2. recovery wraps everything
197
+ middleware.RequestLogger(logger),
198
+ middleware.CORS(),
199
+ )
200
+
201
+ h.Mount(r)
202
+
203
+ return &Server{
204
+ cfg: cfg,
205
+ logger: logger,
206
+ srv: &http.Server{
207
+ Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
208
+ Handler: r,
209
+ ReadTimeout: cfg.ReadTimeout,
210
+ WriteTimeout: cfg.WriteTimeout,
211
+ },
212
+ }
213
+ }
214
+
215
+ func (s *Server) Run(ctx context.Context) error {
216
+ errCh := make(chan error, 1)
217
+ go func() {
218
+ s.logger.InfoContext(ctx, "server starting",
219
+ slog.String("addr", s.srv.Addr))
220
+ if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
221
+ errCh <- err
222
+ }
223
+ close(errCh)
224
+ }()
225
+
226
+ select {
227
+ case <-ctx.Done():
228
+ s.logger.InfoContext(ctx, "shutdown signal received")
229
+ shutdownCtx, cancel := context.WithTimeout(
230
+ context.Background(), s.cfg.ShutdownTimeout)
231
+ defer cancel()
232
+ return s.srv.Shutdown(shutdownCtx)
233
+ case err := <-errCh:
234
+ return err
235
+ }
236
+ }
237
+ ```
238
+
239
+ Notes:
240
+
241
+ - `gin.New()` not `gin.Default()` — `Default()` adds `Logger()` (text format, not slog) and `Recovery()` (no logger injection). We replace both.
242
+ - `gin.SetMode(gin.ReleaseMode)` silences debug output. Production assumed.
243
+ - `http.Server` with explicit timeouts. The default `nil` timeouts are a DoS waiting to happen.
244
+ - Graceful shutdown: SIGINT/SIGTERM cancels the ctx → `Shutdown(shutdownCtx)` gives in-flight requests up to `ShutdownTimeout` to finish.
245
+
246
+ ---
247
+
248
+ ## Middleware ordering — the rule that actually matters
249
+
250
+ ```
251
+ RequestID → Recovery → Logger → CORS → Auth → Handler
252
+ (1) (2) (3) (4) (5)
253
+ ```
254
+
255
+ 1. **RequestID** is first so every subsequent middleware sees it.
256
+ 2. **Recovery** wraps everything after it. Order: a panic in CORS still gets caught.
257
+ 3. **Logger** sees the request_id and the recovered panic.
258
+ 4. **CORS** before Auth — OPTIONS preflight must return without auth.
259
+ 5. **Auth** is the last cross-cutting middleware. Per-route auth (admin-only) is mounted on a sub-router with extra middleware.
260
+
261
+ ```go
262
+ // Public routes — no auth
263
+ api := r.Group("/api/v1")
264
+ {
265
+ api.POST("/auth/login", h.Login)
266
+ api.GET("/healthz", h.Healthz)
267
+ }
268
+
269
+ // Authenticated routes
270
+ authed := r.Group("/api/v1", middleware.Auth(authSvc))
271
+ {
272
+ authed.GET("/users/:id", h.GetUser)
273
+ authed.POST("/users", h.CreateUser)
274
+ }
275
+
276
+ // Admin-only routes
277
+ admin := r.Group("/api/v1/admin",
278
+ middleware.Auth(authSvc),
279
+ middleware.RequireRole("admin"))
280
+ {
281
+ admin.GET("/users", h.ListAllUsers)
282
+ }
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Middleware examples
288
+
289
+ ### `middleware/request_id.go`
290
+
291
+ ```go
292
+ package middleware
293
+
294
+ import (
295
+ "github.com/gin-gonic/gin"
296
+ "github.com/google/uuid"
297
+ "github.com/your-org/myservice/internal/obs"
298
+ )
299
+
300
+ func RequestID() gin.HandlerFunc {
301
+ return func(c *gin.Context) {
302
+ id := c.GetHeader("X-Request-ID")
303
+ if id == "" {
304
+ id = uuid.Must(uuid.NewV7()).String()
305
+ }
306
+ c.Request = c.Request.WithContext(obs.WithRequestID(c.Request.Context(), id))
307
+ c.Header("X-Request-ID", id)
308
+ c.Next()
309
+ }
310
+ }
311
+ ```
312
+
313
+ ### `middleware/recovery.go`
314
+
315
+ ```go
316
+ package middleware
317
+
318
+ import (
319
+ "log/slog"
320
+ "net/http"
321
+ "runtime/debug"
322
+
323
+ "github.com/gin-gonic/gin"
324
+ )
325
+
326
+ func Recovery(logger *slog.Logger) gin.HandlerFunc {
327
+ return func(c *gin.Context) {
328
+ defer func() {
329
+ if r := recover(); r != nil {
330
+ logger.ErrorContext(c.Request.Context(), "panic recovered",
331
+ slog.Any("panic", r),
332
+ slog.String("stack", string(debug.Stack())),
333
+ )
334
+ if !c.Writer.Written() {
335
+ c.JSON(http.StatusInternalServerError,
336
+ gin.H{"error": "internal_error"})
337
+ }
338
+ c.Abort()
339
+ }
340
+ }()
341
+ c.Next()
342
+ }
343
+ }
344
+ ```
345
+
346
+ ### `middleware/request_logging.go`
347
+
348
+ ```go
349
+ func RequestLogger(logger *slog.Logger) gin.HandlerFunc {
350
+ return func(c *gin.Context) {
351
+ start := time.Now()
352
+ c.Next()
353
+ logger.InfoContext(c.Request.Context(), "http request",
354
+ slog.String("method", c.Request.Method),
355
+ slog.String("path", c.Request.URL.Path),
356
+ slog.Int("status", c.Writer.Status()),
357
+ slog.Int("bytes", c.Writer.Size()),
358
+ slog.Duration("elapsed", time.Since(start)),
359
+ slog.String("ip", c.ClientIP()),
360
+ )
361
+ }
362
+ }
363
+ ```
364
+
365
+ The `sloglint` linter enforces typed attrs (`slog.String(...)`) over `slog.Any("path", ...)`. Keep the form.
366
+
367
+ ### `middleware/cors.go`
368
+
369
+ ```go
370
+ func CORS() gin.HandlerFunc {
371
+ return func(c *gin.Context) {
372
+ c.Header("Access-Control-Allow-Origin", "*")
373
+ c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
374
+ c.Header("Access-Control-Allow-Headers", "*")
375
+ if c.Request.Method == http.MethodOptions {
376
+ c.AbortWithStatus(http.StatusNoContent)
377
+ return
378
+ }
379
+ c.Next()
380
+ }
381
+ }
382
+ ```
383
+
384
+ Note the explicit OPTIONS short-circuit — preflight must NOT go through Auth.
385
+
386
+ ---
387
+
388
+ ## Handlers — the canonical shape
389
+
390
+ ```go
391
+ package handlers
392
+
393
+ import (
394
+ "errors"
395
+ "net/http"
396
+
397
+ "github.com/gin-gonic/gin"
398
+ "github.com/go-playground/validator/v10"
399
+ "github.com/your-org/myservice/internal/domain"
400
+ "github.com/your-org/myservice/internal/httperr"
401
+ "github.com/your-org/myservice/internal/service"
402
+ )
403
+
404
+ type Handler struct {
405
+ Users *service.UserService
406
+ }
407
+
408
+ func (h *Handler) Mount(r gin.IRouter) {
409
+ api := r.Group("/api/v1")
410
+ api.POST("/users", h.CreateUser)
411
+ api.GET("/users/:id", h.GetUser)
412
+ }
413
+
414
+ type createUserReq struct {
415
+ Email string `json:"email" binding:"required,email"`
416
+ Username string `json:"username" binding:"required,alphanum,min=3,max=32"`
417
+ }
418
+
419
+ func (h *Handler) CreateUser(c *gin.Context) {
420
+ var req createUserReq
421
+ if err := c.ShouldBindJSON(&req); err != nil {
422
+ writeBindingError(c, err)
423
+ return
424
+ }
425
+
426
+ email, err := domain.NewEmail(req.Email)
427
+ if err != nil {
428
+ httperr.Write(c, err)
429
+ return
430
+ }
431
+ username, err := domain.NewUsername(req.Username)
432
+ if err != nil {
433
+ httperr.Write(c, err)
434
+ return
435
+ }
436
+
437
+ user, err := h.Users.Create(c.Request.Context(), email, username)
438
+ if err != nil {
439
+ httperr.Write(c, err)
440
+ return
441
+ }
442
+ c.JSON(http.StatusCreated, user)
443
+ }
444
+
445
+ func writeBindingError(c *gin.Context, err error) {
446
+ var vErr validator.ValidationErrors
447
+ if errors.As(err, &vErr) {
448
+ out := make(map[string]string, len(vErr))
449
+ for _, fe := range vErr {
450
+ out[fe.Field()] = fe.Tag()
451
+ }
452
+ c.JSON(http.StatusBadRequest, gin.H{"errors": out})
453
+ return
454
+ }
455
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"})
456
+ }
457
+ ```
458
+
459
+ See `data-modeling.md` for the validator tag reference; see `error-handling.md` for the `httperr.Write` funnel.
460
+
461
+ ---
462
+
463
+ ## SSE streaming — the production pattern
464
+
465
+ CLIProxyAPI streams OpenAI-compatible SSE for hundreds of concurrent clients. The pattern:
466
+
467
+ ```go
468
+ func (h *Handler) StreamChat(c *gin.Context) {
469
+ ctx, cancel := context.WithCancel(c.Request.Context())
470
+ defer cancel()
471
+
472
+ // 1. Set SSE headers BEFORE writing any body
473
+ c.Header("Content-Type", "text/event-stream")
474
+ c.Header("Cache-Control", "no-cache")
475
+ c.Header("Connection", "keep-alive")
476
+ c.Header("X-Accel-Buffering", "no") // disable nginx buffering
477
+
478
+ // 2. Obtain the flusher — REQUIRED for streaming
479
+ flusher, ok := c.Writer.(http.Flusher)
480
+ if !ok {
481
+ httperr.Write(c, errors.New("streaming unsupported"))
482
+ return
483
+ }
484
+
485
+ // 3. Pull chunks from upstream
486
+ chunks, errs := h.svc.StreamCompletions(ctx, req)
487
+
488
+ for {
489
+ select {
490
+ case <-ctx.Done():
491
+ return // client disconnected, ctx cancelled
492
+ case chunk, ok := <-chunks:
493
+ if !ok {
494
+ fmt.Fprint(c.Writer, "data: [DONE]\n\n")
495
+ flusher.Flush()
496
+ return
497
+ }
498
+ fmt.Fprintf(c.Writer, "data: %s\n\n", chunk)
499
+ flusher.Flush()
500
+ case err := <-errs:
501
+ // Error mid-stream — emit as SSE event and bail
502
+ fmt.Fprintf(c.Writer, "event: error\ndata: %s\n\n", err.Error())
503
+ flusher.Flush()
504
+ return
505
+ }
506
+ }
507
+ }
508
+ ```
509
+
510
+ Key facts:
511
+
512
+ - **Headers MUST be set before the first `Write`.** Otherwise gin auto-sets `Content-Type: text/plain`.
513
+ - **`c.Writer.(http.Flusher)` is the streaming primitive.** Without `flusher.Flush()`, the response is buffered and arrives as one blob at the end.
514
+ - **Always respond to `<-ctx.Done()`.** A disconnected client must stop upstream work — otherwise you generate tokens for nothing.
515
+ - **The trailing `\n\n` per event is wire-mandatory** for SSE parsing. Missing it = the client never sees the event.
516
+
517
+ ---
518
+
519
+ ## WebSocket upgrade
520
+
521
+ ```go
522
+ import "github.com/gorilla/websocket" // still the canonical WS lib in 2026
523
+
524
+ var upgrader = websocket.Upgrader{
525
+ ReadBufferSize: 4096,
526
+ WriteBufferSize: 4096,
527
+ CheckOrigin: func(r *http.Request) bool {
528
+ // tighten in production
529
+ return true
530
+ },
531
+ }
532
+
533
+ func (h *Handler) WebSocketEcho(c *gin.Context) {
534
+ conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
535
+ if err != nil {
536
+ slog.ErrorContext(c.Request.Context(), "ws upgrade failed", slog.Any("err", err))
537
+ return
538
+ }
539
+ defer conn.Close()
540
+
541
+ for {
542
+ mt, msg, err := conn.ReadMessage()
543
+ if err != nil { return }
544
+ if err := conn.WriteMessage(mt, msg); err != nil { return }
545
+ }
546
+ }
547
+ ```
548
+
549
+ For long-lived connections, use `conn.SetReadDeadline` + `SetPongHandler` for keepalive. CLIProxyAPI's `wsrelay` package is a reference implementation.
550
+
551
+ ---
552
+
553
+ ## Database wiring — pgx pool, injected, never global
554
+
555
+ ```go
556
+ package store
557
+
558
+ import (
559
+ "context"
560
+ "fmt"
561
+ "github.com/jackc/pgx/v5/pgxpool"
562
+ )
563
+
564
+ func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
565
+ cfg, err := pgxpool.ParseConfig(dsn)
566
+ if err != nil {
567
+ return nil, fmt.Errorf("parse dsn: %w", err)
568
+ }
569
+ cfg.MaxConns = 25
570
+ cfg.MinConns = 5
571
+ cfg.MaxConnLifetime = time.Hour
572
+ cfg.MaxConnIdleTime = 30 * time.Minute
573
+
574
+ pool, err := pgxpool.NewWithConfig(ctx, cfg)
575
+ if err != nil {
576
+ return nil, fmt.Errorf("connect: %w", err)
577
+ }
578
+ if err := pool.Ping(ctx); err != nil {
579
+ pool.Close()
580
+ return nil, fmt.Errorf("ping: %w", err)
581
+ }
582
+ return pool, nil
583
+ }
584
+ ```
585
+
586
+ See `sqlc-pgx.md` for queries.
587
+
588
+ ---
589
+
590
+ ## Healthcheck
591
+
592
+ ```go
593
+ func (h *Handler) Healthz(c *gin.Context) {
594
+ if err := h.pool.Ping(c.Request.Context()); err != nil {
595
+ c.JSON(503, gin.H{"db": "down", "error": err.Error()})
596
+ return
597
+ }
598
+ c.JSON(200, gin.H{"ok": true})
599
+ }
600
+ ```
601
+
602
+ Mount BEFORE auth. Health checks must be unauthenticated.
603
+
604
+ ---
605
+
606
+ ## Testing the server
607
+
608
+ ```go
609
+ func TestCreateUser_returns_201_for_valid_input(t *testing.T) {
610
+ // Given
611
+ h := newTestHandler(t)
612
+ r := gin.New()
613
+ h.Mount(r)
614
+
615
+ body := `{"email":"a@b.com","username":"alice"}`
616
+ req := httptest.NewRequest("POST", "/api/v1/users", strings.NewReader(body))
617
+ req.Header.Set("Content-Type", "application/json")
618
+ rec := httptest.NewRecorder()
619
+
620
+ // When
621
+ r.ServeHTTP(rec, req)
622
+
623
+ // Then
624
+ require.Equal(t, http.StatusCreated, rec.Code)
625
+ var got struct{ ID string `json:"id"` }
626
+ require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
627
+ require.NotEmpty(t, got.ID)
628
+ }
629
+ ```
630
+
631
+ See `testing.md` for full patterns (testcontainers integration, table-driven, goleak).
632
+
633
+ ---
634
+
635
+ ## Sources
636
+
637
+ - gin docs: https://gin-gonic.com/docs/
638
+ - CLIProxyAPI (reference impl): https://github.com/router-for-me/CLIProxyAPI
639
+ - pgx pool: https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool
640
+ - SSE spec: https://html.spec.whatwg.org/multipage/server-sent-events.html
641
+ - Go's `http.Server` graceful shutdown: https://pkg.go.dev/net/http#Server.Shutdown