Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions .dagger/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,13 @@ func (t *Tapes) buildLinux(outputs *dagger.Directory, ldflags string) *dagger.Di
zigDownloadURL := fmt.Sprintf("https://ziglang.org/download/%s/zig-%s-linux-%s.tar.xz", zigVersion, zigArch, zigVersion)
zigDir := fmt.Sprintf("zig-%s-linux-%s", zigArch, zigVersion)

golang := dag.Container().
From("golang:1.25-bookworm").
WithExec([]string{"apt-get", "update"}).
WithExec([]string{"apt-get", "install", "-y", "libsqlite3-dev", "xz-utils"}).
golang := t.goContainer().
WithExec([]string{"apt-get", "install", "-y", "xz-utils"}).
WithExec([]string{"mkdir", "-p", "/opt/sqlite"}).
WithExec([]string{"cp", "/usr/include/sqlite3.h", "/opt/sqlite/"}).
WithExec([]string{"cp", "/usr/include/sqlite3ext.h", "/opt/sqlite/"}).
WithExec([]string{"sh", "-c", fmt.Sprintf("curl -L %s | tar -xJ -C /usr/local", zigDownloadURL)}).
WithEnvVariable("PATH", fmt.Sprintf("/usr/local/%s:$PATH", zigDir), dagger.ContainerWithEnvVariableOpts{Expand: true}).
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod")).
WithMountedCache("/root/.cache/go-build", dag.CacheVolume("go-build")).
WithDirectory("/src", t.Source).
WithWorkdir("/src")
WithEnvVariable("PATH", fmt.Sprintf("/usr/local/%s:$PATH", zigDir), dagger.ContainerWithEnvVariableOpts{Expand: true})

for _, target := range targets {
path := fmt.Sprintf("%s/%s/", target.goos, target.goarch)
Expand Down Expand Up @@ -129,6 +123,7 @@ func (t *Tapes) buildDarwin(outputs *dagger.Directory, ldflags string) *dagger.D

// Use Debian Trixie as the base for darwin builds because the osxcross
// toolchain binaries require GLIBC 2.38+ (Bookworm only has 2.36).
// NOTE: this cannot reuse goContainer() since it needs Trixie, not Bookworm.
golang := dag.Container().
From("golang:1.25-trixie").
WithExec([]string{"apt-get", "update"}).
Expand All @@ -139,6 +134,8 @@ func (t *Tapes) buildDarwin(outputs *dagger.Directory, ldflags string) *dagger.D
WithDirectory("/osxcross", osxcross).
WithEnvVariable("PATH", "/osxcross/bin:$PATH", dagger.ContainerWithEnvVariableOpts{Expand: true}).
WithEnvVariable("LD_LIBRARY_PATH", "/osxcross/lib:$LD_LIBRARY_PATH", dagger.ContainerWithEnvVariableOpts{Expand: true}).
WithEnvVariable("CGO_ENABLED", "1").
WithEnvVariable("GOEXPERIMENT", "jsonv2").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod")).
WithMountedCache("/root/.cache/go-build", dag.CacheVolume("go-build")).
WithDirectory("/src", t.Source).
Expand Down
38 changes: 38 additions & 0 deletions .dagger/linting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"context"
"fmt"

"dagger/tapes/internal/dagger"
)

const golangciLintVersion = "v2.8.0"

// lintOpts returns the common GolangcilintOpts used by both CheckLint and FixLint.
// It layers golangci-lint on top of goContainer() so the sqlite dev headers,
// CGO, and Go caches are already in place.
func (t *Tapes) lintOpts() dagger.GolangcilintOpts {
base := t.goContainer().
WithExec([]string{
"go",
"install",
fmt.Sprintf("github.com/golangci/golangci-lint/v2/cmd/golangci-lint@%s", golangciLintVersion),
})

return dagger.GolangcilintOpts{
BaseCtr: base,
Config: t.Source.File(".golangci.yml"),
}
}

// CheckLint runs golangci-lint against the tapes source code without applying fixes.
func (t *Tapes) CheckLint(ctx context.Context) (string, error) {
return dag.Golangcilint(t.Source, t.lintOpts()).Check(ctx)
}

// FixLint runs golangci-lint against the tapes source code with --fix, applying
// automatic fixes where possible, and returns the modified source directory.
func (t *Tapes) FixLint(ctx context.Context) *dagger.Directory {
return dag.Golangcilint(t.Source, t.lintOpts()).Lint()
}
26 changes: 14 additions & 12 deletions .dagger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,27 @@ func New(
}
}

// Test runs the tapes unit tests via "go test"
func (t *Tapes) Test(ctx context.Context) (string, error) {
return t.testContainer().
WithExec([]string{"go", "test", "-v", "./..."}).
Stdout(ctx)
}

// testContainer returns a container configured for running tests
// with a local gcc toolchain for CGO and sqlite dependencies.
func (t *Tapes) testContainer() *dagger.Container {
// goContainer returns a Debian Bookworm-based Go container with gcc,
// libsqlite3-dev, CGO enabled, and the project source mounted.
//
// It is the shared foundation for tests, builds, and linting.
func (t *Tapes) goContainer() *dagger.Container {
return dag.Container().
From("golang:1.25-bookworm").
WithExec([]string{"apt-get", "update"}).
WithExec([]string{"apt-get", "install", "-y", "gcc"}).
WithExec([]string{"apt-get", "install", "-y", "libsqlite3-dev"}).
WithExec([]string{"apt-get", "install", "-y", "gcc", "libsqlite3-dev"}).
WithEnvVariable("CGO_ENABLED", "1").
WithEnvVariable("GOEXPERIMENT", "jsonv2").
WithEnvVariable("PATH", "/go/bin:$PATH", dagger.ContainerWithEnvVariableOpts{Expand: true}).
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod")).
WithMountedCache("/root/.cache/go-build", dag.CacheVolume("go-build")).
WithWorkdir("/src").
WithDirectory("/src", t.Source)
}

// Test runs the tapes unit tests via "go test"
func (t *Tapes) Test(ctx context.Context) (string, error) {
return t.goContainer().
WithExec([]string{"go", "test", "-v", "./..."}).
Stdout(ctx)
}
19 changes: 19 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ jobs:
args: test
version: ${{ env.DAGGER_VERSION }}

lint-check:
name: GolangCI Lint Check
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Dagger
uses: dagger/dagger-for-github@v8.2.0
with:
verb: version
version: ${{ env.DAGGER_VERSION }}

- name: Run lint check
env:
DAGGER_CLOUD_TOKEN: ${{ secrets.DAGGER_CLOUD_TOKEN }}
run: make check

build:
name: Build
runs-on: ubuntu-latest
Expand Down
118 changes: 118 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json
#
# Paper Compute Co. — opinionated golangci-lint defaults
# https://golangci-lint.run/docs/configuration/file/

version: "2"

linters:
enable:
# --- Bugs ---
- bodyclose # Report unclosed HTTP response bodies.
- contextcheck # Non-inherited context usage.
- durationcheck # Accidental time.duration * time.duration.
- errcheck # Raises unchecked errors.
- errchkjson # Unchecked JSON marshal/unmarshal errors.
- errorlint # Go 1.13+ error wrapping best practices.
- exhaustive # Check exhaustiveness of enum switch statements.
- fatcontext # Nested contexts in loops / closures.
- govet # Examines and reports suspicious constructs.
- ineffassign # Reports when assignments to existing vars are not used.
- makezero # Slices with non-zero len that are later appended to.
- musttag # Missing struct tags for (un)marshaling.
- nilerr # Returning nil when err != nil.
- nilnesserr # Reports returning a different nil error after err check.
- noctx # Reports HTTP requests without context.
- unused # Checks Go code for unused consts, vars, funcs, types.

# --- Code quality ---
- copyloopvar # loop variable copy issues
- dupword # duplicate words in comments / strings
- goconst # repeated strings that could be constants
- gocritic # broad set of style & performance diagnostics
- gosec # security-oriented checks
- misspell # common English typos
- nakedret # naked returns in long functions
- prealloc # slice pre-allocation hints
- predeclared # shadowing predeclared identifiers
- reassign # package-variable reassignment
- revive # golint successor — style & correctness
- unconvert # unnecessary type conversions
- unparam # unused function parameters
- wastedassign # wasted assignments
- whitespace # unnecessary blank lines

# --- Modernization ---
- exptostd # x/exp -> stdlib replacements
- intrange # use integer ranges in for loops
- modernize # modern Go idioms
- usestdlibvars # use stdlib constants/variables
- usetesting # prefer testing package helpers
- perfsprint # faster fmt.Sprintf alternatives

# --- Style ---
- errname # error/sentinel naming conventions
- nolintlint # well-formed nolint directives
- recvcheck # consistent receiver types
- nosprintfhostport # host:port via net.JoinHostPort

disable:
# forbid all init() - we do this when the tuis come up to force color alignments.
# TODO @jpmcb - we should refactor this to avoid using an "init()"
- gochecknoinits

exclusions:
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
generated: lax
rules:
- path: _test\.go
linters:
- errcheck
- gosec
- dupl
- goconst
- gocritic
- revive
- unparam
# @jpmcb - TODO: rename these packages to avoid "meaningless" names
- path: api/
text: "var-naming: avoid meaningless package names"
linters:
- revive
- path: pkg/utils/
text: "var-naming: avoid meaningless package names"
linters:
- revive

formatters:
enable:
- gci
- gofmt
- goimports
- gofumpt
settings:
gci:
sections:
- standard
- default
- prefix(github.com/papercomputeco/tapes)
goimports:
local-prefixes:
- github.com/papercomputeco/tapes
exclusions:
generated: lax
paths:
- .dagger/internal
- .dagger/dagger.gen.go

issues:
# Show all issues
max-issues-per-linter: 0
max-same-issues: 0

run:
timeout: 5m
2 changes: 1 addition & 1 deletion api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var _ = Describe("buildHistory", func() {
BeforeEach(func() {
var err error
logger, _ := zap.NewDevelopment()
inMem := inmemory.NewInMemoryDriver()
inMem := inmemory.NewDriver()
driver = inMem
dagLoader = inMem
server, err = NewServer(Config{ListenAddr: ":0"}, driver, dagLoader, logger)
Expand Down
12 changes: 6 additions & 6 deletions api/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
package mcp

import (
"fmt"
"errors"
"net/http"

"github.com/modelcontextprotocol/go-sdk/mcp"
Expand Down Expand Up @@ -61,16 +61,16 @@ func NewServer(c Config) (*Server, error) {
}

if c.DagLoader == nil {
return nil, fmt.Errorf("storage driver is required")
return nil, errors.New("storage driver is required")
}
if c.VectorDriver == nil {
return nil, fmt.Errorf("vector driver is required")
return nil, errors.New("vector driver is required")
}
if c.Embedder == nil {
return nil, fmt.Errorf("embedder is required")
return nil, errors.New("embedder is required")
}
if c.Logger == nil {
return nil, fmt.Errorf("logger is required")
return nil, errors.New("logger is required")
}

// Add tools
Expand All @@ -83,7 +83,7 @@ func NewServer(c Config) (*Server, error) {

// Create a streamable HTTP net/http handler for stateless operations
s.handler = mcp.NewStreamableHTTPHandler(
func(r *http.Request) *mcp.Server {
func(_ *http.Request) *mcp.Server {
return mcpServer
},
&mcp.StreamableHTTPOptions{
Expand Down
4 changes: 2 additions & 2 deletions api/mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import (
var _ = Describe("MCP Server", func() {
var (
server *mcp.Server
driver *inmemory.InMemoryDriver
driver *inmemory.Driver
vectorDriver *testutils.MockVectorDriver
embedder *testutils.MockEmbedder
)

BeforeEach(func() {
logger, _ := zap.NewDevelopment()
driver = inmemory.NewInMemoryDriver()
driver = inmemory.NewDriver()
vectorDriver = testutils.NewMockVectorDriver()
embedder = testutils.NewMockEmbedder()

Expand Down
15 changes: 7 additions & 8 deletions api/mcp/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,31 @@ var (
searchDescription = "Search over stored LLM sessions using semantic search. Returns the most relevant sessions based on the query text, including the full conversation branch (ancestors and descendants)."
)

// MCPSearchInput represents the input arguments for the MCP search tool.
// SearchInput represents the input arguments for the MCP search tool.
// It uses jsonschema tags specific to the MCP protocol.
type MCPSearchInput struct {
type SearchInput struct {
Query string `json:"query" jsonschema:"the search query text to find relevant sessions"`
TopK int `json:"top_k,omitempty" jsonschema:"number of results to return (default: 5)"`
}

// handleSearch processes a search request via MCP.
// It delegates to the shared search package for the core search logic.
func (s *Server) handleSearch(ctx context.Context, req *mcp.CallToolRequest, input MCPSearchInput) (*mcp.CallToolResult, apisearch.SearchOutput, error) {
output, err := apisearch.Search(
func (s *Server) handleSearch(ctx context.Context, _ *mcp.CallToolRequest, input SearchInput) (*mcp.CallToolResult, apisearch.Output, error) {
searcher := apisearch.NewSearcher(
ctx,
input.Query,
input.TopK,
s.config.Embedder,
s.config.VectorDriver,
s.config.DagLoader,
s.config.Logger,
)
output, err := searcher.Search(input.Query, input.TopK)
if err != nil {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Search failed: %v", err)},
},
}, apisearch.SearchOutput{}, nil
}, apisearch.Output{}, nil
}

// Serialize the structured output as JSON for the text field
Expand All @@ -53,7 +52,7 @@ func (s *Server) handleSearch(ctx context.Context, req *mcp.CallToolRequest, inp
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Failed to serialize results: %v", err)},
},
}, apisearch.SearchOutput{}, nil
}, apisearch.Output{}, nil
}

return &mcp.CallToolResult{
Expand Down
Loading