Compare commits
7 Commits
feat/issue
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 1328b3cc94 | |||
| 87a62c0d6c | |||
| 3e09dc0ac6 | |||
| b59e02aebe | |||
| 4713110e32 | |||
| 6c6b49b45c | |||
| 59c43188e5 |
41
.gitea/workflows/ci.yml
Normal file
41
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
tags-ignore: ['v*']
|
||||
pull_request:
|
||||
branches: ['**']
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26'
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run tests
|
||||
run: go test ./...
|
||||
|
||||
- name: Build amcs-server
|
||||
run: go build -o /dev/null ./cmd/amcs-server
|
||||
|
||||
- name: Build amcs-cli
|
||||
run: go build -o /dev/null ./cmd/amcs-cli
|
||||
102
.gitea/workflows/release.yml
Normal file
102
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,102 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
env:
|
||||
GITEA_SERVER: https://git.warky.dev
|
||||
GITEA_REPO: wdevs/amcs
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26'
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Set build vars
|
||||
id: vars
|
||||
run: |
|
||||
echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build release binaries
|
||||
run: |
|
||||
VERSION="${{ steps.vars.outputs.VERSION }}"
|
||||
COMMIT="${{ steps.vars.outputs.COMMIT }}"
|
||||
BUILD_DATE="${{ steps.vars.outputs.BUILD_DATE }}"
|
||||
LDFLAGS="-s -w -X git.warky.dev/wdevs/amcs/internal/buildinfo.Version=${VERSION} -X git.warky.dev/wdevs/amcs/internal/buildinfo.TagName=${VERSION} -X git.warky.dev/wdevs/amcs/internal/buildinfo.Commit=${COMMIT} -X git.warky.dev/wdevs/amcs/internal/buildinfo.BuildDate=${BUILD_DATE}"
|
||||
mkdir -p dist
|
||||
for BINARY in amcs-server amcs-cli; do
|
||||
CMD="./cmd/${BINARY}"
|
||||
for PLATFORM in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
|
||||
OS="${PLATFORM%/*}"
|
||||
ARCH="${PLATFORM#*/}"
|
||||
EXT=""
|
||||
[ "$OS" = "windows" ] && EXT=".exe"
|
||||
OUTPUT="dist/${BINARY}-${OS}-${ARCH}${EXT}"
|
||||
echo "Building ${OUTPUT}..."
|
||||
GOOS=$OS GOARCH=$ARCH go build -ldflags "${LDFLAGS}" -o "${OUTPUT}" "${CMD}"
|
||||
done
|
||||
done
|
||||
cd dist && sha256sum * > checksums.txt && cd ..
|
||||
|
||||
- name: Create Gitea Release
|
||||
id: create_release
|
||||
run: |
|
||||
VERSION="${{ steps.vars.outputs.VERSION }}"
|
||||
BODY=$(python3 <<'PY'
|
||||
import json, subprocess, os
|
||||
version = os.environ['VERSION']
|
||||
commit = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], text=True).strip()
|
||||
body = f"## {version}\n\nBuilt from commit {commit}.\n\nSee `checksums.txt` to verify downloads."
|
||||
print(json.dumps({
|
||||
'tag_name': version,
|
||||
'name': version,
|
||||
'body': body,
|
||||
'draft': False,
|
||||
'prerelease': False,
|
||||
}))
|
||||
PY
|
||||
)
|
||||
RESPONSE=$(curl -fsS -X POST "${{ env.GITEA_SERVER }}/api/v1/repos/${{ env.GITEA_REPO }}/releases" \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY")
|
||||
RELEASE_ID=$(printf '%s' "$RESPONSE" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
||||
echo "RELEASE_ID=${RELEASE_ID}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload release assets
|
||||
run: |
|
||||
RELEASE_ID="${{ steps.create_release.outputs.RELEASE_ID }}"
|
||||
for f in dist/*; do
|
||||
name=$(basename "$f")
|
||||
echo "Uploading ${name}..."
|
||||
curl -fsS -X POST \
|
||||
"${{ env.GITEA_SERVER }}/api/v1/repos/${{ env.GITEA_REPO }}/releases/${RELEASE_ID}/assets?name=${name}" \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"${f}"
|
||||
done
|
||||
35
Makefile
35
Makefile
@@ -7,13 +7,17 @@ PATCH_INCREMENT ?= 1
|
||||
VERSION_TAG ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev)
|
||||
COMMIT_SHA ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
RELSPEC ?= $(shell command -v relspec 2>/dev/null || echo $(HOME)/go/bin/relspec)
|
||||
SCHEMA_FILES := $(sort $(wildcard schema/*.dbml))
|
||||
MERGE_TARGET_TMP := $(CURDIR)/.cache/schema.merge-target.dbml
|
||||
GENERATED_SCHEMA_MIGRATION := migrations/020_generated_schema.sql
|
||||
LDFLAGS := -s -w \
|
||||
-X $(BUILDINFO_PKG).Version=$(VERSION_TAG) \
|
||||
-X $(BUILDINFO_PKG).TagName=$(VERSION_TAG) \
|
||||
-X $(BUILDINFO_PKG).Commit=$(COMMIT_SHA) \
|
||||
-X $(BUILDINFO_PKG).BuildDate=$(BUILD_DATE)
|
||||
|
||||
.PHONY: all build clean migrate release-version test
|
||||
.PHONY: all build clean migrate release-version test generate-migrations check-schema-drift
|
||||
|
||||
all: build
|
||||
|
||||
@@ -50,3 +54,32 @@ migrate:
|
||||
|
||||
clean:
|
||||
rm -rf $(BIN_DIR)
|
||||
|
||||
generate-migrations:
|
||||
@test -n "$(SCHEMA_FILES)" || (echo "No DBML schema files found in schema/" >&2; exit 1)
|
||||
@command -v $(RELSPEC) >/dev/null 2>&1 || (echo "relspec not found; install git.warky.dev/wdevs/relspecgo/cmd/relspec@latest" >&2; exit 1)
|
||||
@mkdir -p $(dir $(MERGE_TARGET_TMP))
|
||||
@: > $(MERGE_TARGET_TMP)
|
||||
@schema_list=$$(printf '%s\n' $(SCHEMA_FILES) | paste -sd, -); \
|
||||
$(RELSPEC) merge --target dbml --target-path $(MERGE_TARGET_TMP) --source dbml --from-list "$$schema_list" --output pgsql --output-path $(GENERATED_SCHEMA_MIGRATION)
|
||||
|
||||
check-schema-drift:
|
||||
@test -f $(GENERATED_SCHEMA_MIGRATION) || (echo "$(GENERATED_SCHEMA_MIGRATION) is missing; run make generate-migrations" >&2; exit 1)
|
||||
@command -v $(RELSPEC) >/dev/null 2>&1 || (echo "relspec not found; install git.warky.dev/wdevs/relspecgo/cmd/relspec@latest" >&2; exit 1)
|
||||
@mkdir -p $(dir $(MERGE_TARGET_TMP))
|
||||
@tmpfile=$$(mktemp); \
|
||||
: > $(MERGE_TARGET_TMP); \
|
||||
schema_list=$$(printf '%s\n' $(SCHEMA_FILES) | paste -sd, -); \
|
||||
$(RELSPEC) merge --target dbml --target-path $(MERGE_TARGET_TMP) --source dbml --from-list "$$schema_list" --output pgsql --output-path $$tmpfile; \
|
||||
if ! cmp -s $$tmpfile $(GENERATED_SCHEMA_MIGRATION); then \
|
||||
echo "Schema drift detected between schema/*.dbml and $(GENERATED_SCHEMA_MIGRATION)" >&2; \
|
||||
diff -u $(GENERATED_SCHEMA_MIGRATION) $$tmpfile || true; \
|
||||
rm -f $$tmpfile; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
rm -f $$tmpfile
|
||||
|
||||
.PHONY: build-cli
|
||||
build-cli:
|
||||
@mkdir -p $(BIN_DIR)
|
||||
go build -o $(BIN_DIR)/amcs-cli ./cmd/amcs-cli
|
||||
|
||||
98
cmd/amcs-cli/cmd/call.go
Normal file
98
cmd/amcs-cli/cmd/call.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var argFlags []string
|
||||
|
||||
var callCmd = &cobra.Command{
|
||||
Use: "call <tool>",
|
||||
Short: "Call a remote AMCS tool",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
toolName := args[0]
|
||||
toolArgs, err := parseArgs(argFlags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := connectRemote(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = session.Close() }()
|
||||
|
||||
res, err := session.CallTool(cmd.Context(), &mcp.CallToolParams{Name: toolName, Arguments: toolArgs})
|
||||
if err != nil {
|
||||
return fmt.Errorf("call tool %q: %w", toolName, err)
|
||||
}
|
||||
return printOutput(res)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
callCmd.Flags().StringArrayVar(&argFlags, "arg", nil, "Tool argument in key=value format (repeatable)")
|
||||
rootCmd.AddCommand(callCmd)
|
||||
}
|
||||
|
||||
func parseArgs(items []string) (map[string]any, error) {
|
||||
result := make(map[string]any, len(items))
|
||||
for _, item := range items {
|
||||
key, value, ok := strings.Cut(item, "=")
|
||||
if !ok || strings.TrimSpace(key) == "" {
|
||||
return nil, fmt.Errorf("invalid --arg %q: want key=value", item)
|
||||
}
|
||||
result[key] = parseScalar(value)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseScalar(s string) any {
|
||||
if s == "true" || s == "false" {
|
||||
b, _ := strconv.ParseBool(s)
|
||||
return b
|
||||
}
|
||||
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||
return i
|
||||
}
|
||||
if f, err := strconv.ParseFloat(s, 64); err == nil && strings.ContainsAny(s, ".eE") {
|
||||
return f
|
||||
}
|
||||
var v any
|
||||
if err := json.Unmarshal([]byte(s), &v); err == nil {
|
||||
switch v.(type) {
|
||||
case map[string]any, []any, float64, bool, nil:
|
||||
return v
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func printOutput(v any) error {
|
||||
switch outputFlag {
|
||||
case "yaml":
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal yaml: %w", err)
|
||||
}
|
||||
_, err = os.Stdout.Write(data)
|
||||
return err
|
||||
default:
|
||||
data, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal json: %w", err)
|
||||
}
|
||||
data = append(data, '\n')
|
||||
_, err = os.Stdout.Write(data)
|
||||
return err
|
||||
}
|
||||
}
|
||||
60
cmd/amcs-cli/cmd/config.go
Normal file
60
cmd/amcs-cli/cmd/config.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server string `yaml:"server"`
|
||||
Token string `yaml:"token"`
|
||||
}
|
||||
|
||||
func defaultConfigPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve home dir: %w", err)
|
||||
}
|
||||
return filepath.Join(home, ".config", "amcs", "config.yaml"), nil
|
||||
}
|
||||
|
||||
func resolveConfigPath(path string) (string, error) {
|
||||
if strings.TrimSpace(path) != "" {
|
||||
return path, nil
|
||||
}
|
||||
return defaultConfigPath()
|
||||
}
|
||||
|
||||
func loadConfigFile(path string) (Config, error) {
|
||||
var cfg Config
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return cfg, nil
|
||||
}
|
||||
return cfg, fmt.Errorf("read config: %w", err)
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return cfg, fmt.Errorf("parse config: %w", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func saveConfigFile(path string, cfg Config) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return fmt.Errorf("create config dir: %w", err)
|
||||
}
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal config: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
return fmt.Errorf("write config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
134
cmd/amcs-cli/cmd/root.go
Normal file
134
cmd/amcs-cli/cmd/root.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
serverFlag string
|
||||
tokenFlag string
|
||||
outputFlag string
|
||||
cfg Config
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "amcs-cli",
|
||||
Short: "CLI for connecting to a remote AMCS MCP server",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return loadConfig()
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Path to config file")
|
||||
rootCmd.PersistentFlags().StringVar(&serverFlag, "server", "", "AMCS server URL")
|
||||
rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "AMCS bearer token")
|
||||
rootCmd.PersistentFlags().StringVar(&outputFlag, "output", "json", "Output format: json or yaml")
|
||||
}
|
||||
|
||||
func loadConfig() error {
|
||||
path, err := resolveConfigPath(cfgFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
loaded, err := loadConfigFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg = loaded
|
||||
if v := strings.TrimSpace(os.Getenv("AMCS_URL")); v != "" {
|
||||
cfg.Server = v
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("AMCS_TOKEN")); v != "" {
|
||||
cfg.Token = v
|
||||
}
|
||||
if v := strings.TrimSpace(serverFlag); v != "" {
|
||||
cfg.Server = v
|
||||
}
|
||||
if v := strings.TrimSpace(tokenFlag); v != "" {
|
||||
cfg.Token = v
|
||||
}
|
||||
outputFlag = strings.ToLower(strings.TrimSpace(outputFlag))
|
||||
if outputFlag != "json" && outputFlag != "yaml" {
|
||||
return fmt.Errorf("invalid --output %q: must be json or yaml", outputFlag)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func requireServer() error {
|
||||
if strings.TrimSpace(cfg.Server) == "" {
|
||||
return fmt.Errorf("server URL is required; set --server, AMCS_URL, or config server")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func endpointURL() string {
|
||||
base := strings.TrimRight(strings.TrimSpace(cfg.Server), "/")
|
||||
if strings.HasSuffix(base, "/mcp") {
|
||||
return base
|
||||
}
|
||||
return base + "/mcp"
|
||||
}
|
||||
|
||||
func newHTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: 0,
|
||||
Transport: &bearerTransport{
|
||||
base: http.DefaultTransport,
|
||||
token: cfg.Token,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type bearerTransport struct {
|
||||
base http.RoundTripper
|
||||
token string
|
||||
}
|
||||
|
||||
func (t *bearerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
base := t.base
|
||||
if base == nil {
|
||||
base = http.DefaultTransport
|
||||
}
|
||||
clone := req.Clone(req.Context())
|
||||
if strings.TrimSpace(t.token) != "" {
|
||||
clone.Header.Set("Authorization", "Bearer "+t.token)
|
||||
}
|
||||
return base.RoundTrip(clone)
|
||||
}
|
||||
|
||||
func connectRemote(ctx context.Context) (*mcp.ClientSession, error) {
|
||||
if err := requireServer(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := mcp.NewClient(&mcp.Implementation{Name: "amcs-cli", Version: "0.0.1"}, nil)
|
||||
transport := &mcp.StreamableClientTransport{
|
||||
Endpoint: endpointURL(),
|
||||
HTTPClient: newHTTPClient(),
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
session, err := client.Connect(ctx, transport, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to AMCS server: %w", err)
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
62
cmd/amcs-cli/cmd/stdio.go
Normal file
62
cmd/amcs-cli/cmd/stdio.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var stdioCmd = &cobra.Command{
|
||||
Use: "stdio",
|
||||
Short: "Run a stdio MCP bridge backed by a remote AMCS server",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
remote, err := connectRemote(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = remote.Close() }()
|
||||
|
||||
tools, err := remote.ListTools(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load remote tools: %w", err)
|
||||
}
|
||||
|
||||
server := mcp.NewServer(&mcp.Implementation{
|
||||
Name: "amcs-cli",
|
||||
Title: "AMCS CLI Bridge",
|
||||
Version: "0.0.1",
|
||||
}, nil)
|
||||
|
||||
for _, tool := range tools.Tools {
|
||||
remoteTool := tool
|
||||
server.AddTool(&mcp.Tool{
|
||||
Name: remoteTool.Name,
|
||||
Description: remoteTool.Description,
|
||||
InputSchema: remoteTool.InputSchema,
|
||||
OutputSchema: remoteTool.OutputSchema,
|
||||
Annotations: remoteTool.Annotations,
|
||||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return remote.CallTool(ctx, &mcp.CallToolParams{
|
||||
Name: req.Params.Name,
|
||||
Arguments: req.Params.Arguments,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
session, err := server.Connect(ctx, &mcp.StdioTransport{}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start stdio bridge: %w", err)
|
||||
}
|
||||
defer func() { _ = session.Close() }()
|
||||
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(stdioCmd)
|
||||
}
|
||||
38
cmd/amcs-cli/cmd/tools.go
Normal file
38
cmd/amcs-cli/cmd/tools.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var toolsCmd = &cobra.Command{
|
||||
Use: "tools",
|
||||
Short: "List tools available on the remote AMCS server",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
session, err := connectRemote(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = session.Close() }()
|
||||
|
||||
res, err := session.ListTools(cmd.Context(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list tools: %w", err)
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "NAME\tDESCRIPTION")
|
||||
for _, tool := range res.Tools {
|
||||
fmt.Fprintf(w, "%s\t%s\n", tool.Name, strings.TrimSpace(tool.Description))
|
||||
}
|
||||
return w.Flush()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(toolsCmd)
|
||||
}
|
||||
7
cmd/amcs-cli/main.go
Normal file
7
cmd/amcs-cli/main.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "git.warky.dev/wdevs/amcs/cmd/amcs-cli/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
3
go.mod
3
go.mod
@@ -8,11 +8,13 @@ require (
|
||||
github.com/jackc/pgx/v5 v5.9.1
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.1
|
||||
github.com/pgvector/pgvector-go v0.3.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
golang.org/x/sync v0.17.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
@@ -20,6 +22,7 @@ require (
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.4 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
|
||||
9
go.sum
9
go.sum
@@ -1,5 +1,6 @@
|
||||
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
|
||||
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -16,6 +17,8 @@ github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbc
|
||||
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -44,10 +47,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
|
||||
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
|
||||
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
|
||||
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -73,6 +81,7 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
|
||||
3996
migrations/020_generated_schema.sql
Normal file
3996
migrations/020_generated_schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
35
schema/README.md
Normal file
35
schema/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Schema workflow
|
||||
|
||||
The `schema/*.dbml` files are the database schema source of truth.
|
||||
|
||||
## Generate SQL migrations
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
make generate-migrations
|
||||
```
|
||||
|
||||
This uses `relspec` to convert the DBML files into PostgreSQL SQL and writes the generated schema migration to:
|
||||
|
||||
- `migrations/020_generated_schema.sql`
|
||||
|
||||
## Check schema drift
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
make check-schema-drift
|
||||
```
|
||||
|
||||
This regenerates the SQL from `schema/*.dbml` and compares it with `migrations/020_generated_schema.sql`.
|
||||
If the generated output differs, the command fails so CI can catch schema drift.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Update the DBML files in `schema/`
|
||||
2. Run `make generate-migrations`
|
||||
3. Review the generated SQL
|
||||
4. Commit both the DBML changes and the generated migration
|
||||
|
||||
Existing handwritten migrations stay in place. Going forward, update the DBML first and regenerate the SQL from there.
|
||||
44
schema/calendar.dbml
Normal file
44
schema/calendar.dbml
Normal file
@@ -0,0 +1,44 @@
|
||||
Table family_members {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [not null]
|
||||
relationship text
|
||||
birth_date date
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
|
||||
Table activities {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
family_member_id uuid [ref: > family_members.id]
|
||||
title text [not null]
|
||||
activity_type text
|
||||
day_of_week text
|
||||
start_time time
|
||||
end_time time
|
||||
start_date date
|
||||
end_date date
|
||||
location text
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
day_of_week
|
||||
family_member_id
|
||||
(start_date, end_date)
|
||||
}
|
||||
}
|
||||
|
||||
Table important_dates {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
family_member_id uuid [ref: > family_members.id]
|
||||
title text [not null]
|
||||
date_value date [not null]
|
||||
recurring_yearly boolean [not null, default: false]
|
||||
reminder_days_before int [not null, default: 7]
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
date_value
|
||||
}
|
||||
}
|
||||
48
schema/core.dbml
Normal file
48
schema/core.dbml
Normal file
@@ -0,0 +1,48 @@
|
||||
Table thoughts {
|
||||
id bigserial [pk]
|
||||
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
||||
content text [not null]
|
||||
metadata jsonb [default: `'{}'::jsonb`]
|
||||
created_at timestamptz [default: `now()`]
|
||||
updated_at timestamptz [default: `now()`]
|
||||
project_id uuid [ref: > projects.guid]
|
||||
archived_at timestamptz
|
||||
}
|
||||
|
||||
Table projects {
|
||||
id bigserial [pk]
|
||||
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
||||
name text [unique, not null]
|
||||
description text
|
||||
created_at timestamptz [default: `now()`]
|
||||
last_active_at timestamptz [default: `now()`]
|
||||
}
|
||||
|
||||
Table thought_links {
|
||||
from_id bigint [not null, ref: > thoughts.id]
|
||||
to_id bigint [not null, ref: > thoughts.id]
|
||||
relation text [not null]
|
||||
created_at timestamptz [default: `now()`]
|
||||
|
||||
indexes {
|
||||
(from_id, to_id, relation) [pk]
|
||||
from_id
|
||||
to_id
|
||||
}
|
||||
}
|
||||
|
||||
Table embeddings {
|
||||
id bigserial [pk]
|
||||
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
||||
thought_id uuid [not null, ref: > thoughts.guid]
|
||||
model text [not null]
|
||||
dim int [not null]
|
||||
embedding vector [not null]
|
||||
created_at timestamptz [default: `now()`]
|
||||
updated_at timestamptz [default: `now()`]
|
||||
|
||||
indexes {
|
||||
(thought_id, model) [unique]
|
||||
thought_id
|
||||
}
|
||||
}
|
||||
53
schema/crm.dbml
Normal file
53
schema/crm.dbml
Normal file
@@ -0,0 +1,53 @@
|
||||
Table professional_contacts {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [not null]
|
||||
company text
|
||||
title text
|
||||
email text
|
||||
phone text
|
||||
linkedin_url text
|
||||
how_we_met text
|
||||
tags "text[]" [not null, default: `'{}'`]
|
||||
notes text
|
||||
last_contacted timestamptz
|
||||
follow_up_date date
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
last_contacted
|
||||
follow_up_date
|
||||
}
|
||||
}
|
||||
|
||||
Table contact_interactions {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
contact_id uuid [not null, ref: > professional_contacts.id]
|
||||
interaction_type text [not null]
|
||||
occurred_at timestamptz [not null, default: `now()`]
|
||||
summary text [not null]
|
||||
follow_up_needed boolean [not null, default: false]
|
||||
follow_up_notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(contact_id, occurred_at)
|
||||
}
|
||||
}
|
||||
|
||||
Table opportunities {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
contact_id uuid [ref: > professional_contacts.id]
|
||||
title text [not null]
|
||||
description text
|
||||
stage text [not null, default: 'identified']
|
||||
value "decimal(12,2)"
|
||||
expected_close_date date
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
stage
|
||||
}
|
||||
}
|
||||
25
schema/files.dbml
Normal file
25
schema/files.dbml
Normal file
@@ -0,0 +1,25 @@
|
||||
Table stored_files {
|
||||
id bigserial [pk]
|
||||
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
||||
thought_id uuid [ref: > thoughts.guid]
|
||||
project_id uuid [ref: > projects.guid]
|
||||
name text [not null]
|
||||
media_type text [not null]
|
||||
kind text [not null, default: 'file']
|
||||
encoding text [not null, default: 'base64']
|
||||
size_bytes bigint [not null]
|
||||
sha256 text [not null]
|
||||
content bytea [not null]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
thought_id
|
||||
project_id
|
||||
sha256
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-file refs (for relspecgo merge)
|
||||
Ref: stored_files.thought_id > thoughts.guid [delete: set null]
|
||||
Ref: stored_files.project_id > projects.guid [delete: set null]
|
||||
31
schema/household.dbml
Normal file
31
schema/household.dbml
Normal file
@@ -0,0 +1,31 @@
|
||||
Table household_items {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [not null]
|
||||
category text
|
||||
location text
|
||||
details jsonb [not null, default: `'{}'`]
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
category
|
||||
}
|
||||
}
|
||||
|
||||
Table household_vendors {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [not null]
|
||||
service_type text
|
||||
phone text
|
||||
email text
|
||||
website text
|
||||
notes text
|
||||
rating int
|
||||
last_used date
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
service_type
|
||||
}
|
||||
}
|
||||
30
schema/maintenance.dbml
Normal file
30
schema/maintenance.dbml
Normal file
@@ -0,0 +1,30 @@
|
||||
Table maintenance_tasks {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [not null]
|
||||
category text
|
||||
frequency_days int
|
||||
last_completed timestamptz
|
||||
next_due timestamptz
|
||||
priority text [not null, default: 'medium']
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
next_due
|
||||
}
|
||||
}
|
||||
|
||||
Table maintenance_logs {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
task_id uuid [not null, ref: > maintenance_tasks.id]
|
||||
completed_at timestamptz [not null, default: `now()`]
|
||||
performed_by text
|
||||
cost "decimal(10,2)"
|
||||
notes text
|
||||
next_action text
|
||||
|
||||
indexes {
|
||||
(task_id, completed_at)
|
||||
}
|
||||
}
|
||||
49
schema/meals.dbml
Normal file
49
schema/meals.dbml
Normal file
@@ -0,0 +1,49 @@
|
||||
Table recipes {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [not null]
|
||||
cuisine text
|
||||
prep_time_minutes int
|
||||
cook_time_minutes int
|
||||
servings int
|
||||
ingredients jsonb [not null, default: `'[]'`]
|
||||
instructions jsonb [not null, default: `'[]'`]
|
||||
tags "text[]" [not null, default: `'{}'`]
|
||||
rating int
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
cuisine
|
||||
tags
|
||||
}
|
||||
}
|
||||
|
||||
Table meal_plans {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
week_start date [not null]
|
||||
day_of_week text [not null]
|
||||
meal_type text [not null]
|
||||
recipe_id uuid [ref: > recipes.id]
|
||||
custom_meal text
|
||||
servings int
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
week_start
|
||||
}
|
||||
}
|
||||
|
||||
Table shopping_lists {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
week_start date [unique, not null]
|
||||
items jsonb [not null, default: `'[]'`]
|
||||
notes text
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
week_start
|
||||
}
|
||||
}
|
||||
32
schema/meta.dbml
Normal file
32
schema/meta.dbml
Normal file
@@ -0,0 +1,32 @@
|
||||
Table chat_histories {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
session_id text [not null]
|
||||
title text
|
||||
channel text
|
||||
agent_id text
|
||||
project_id uuid [ref: > projects.guid]
|
||||
messages jsonb [not null, default: `'[]'`]
|
||||
summary text
|
||||
metadata jsonb [not null, default: `'{}'`]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
session_id
|
||||
project_id
|
||||
channel
|
||||
agent_id
|
||||
created_at
|
||||
}
|
||||
}
|
||||
|
||||
Table tool_annotations {
|
||||
id bigserial [pk]
|
||||
tool_name text [unique, not null]
|
||||
notes text [not null, default: '']
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
|
||||
// Cross-file refs (for relspecgo merge)
|
||||
Ref: chat_histories.project_id > projects.guid [delete: set null]
|
||||
46
schema/skills.dbml
Normal file
46
schema/skills.dbml
Normal file
@@ -0,0 +1,46 @@
|
||||
Table agent_skills {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [unique, not null]
|
||||
description text [not null, default: '']
|
||||
content text [not null]
|
||||
tags "text[]" [not null, default: `'{}'`]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
|
||||
Table agent_guardrails {
|
||||
id uuid [pk, default: `gen_random_uuid()`]
|
||||
name text [unique, not null]
|
||||
description text [not null, default: '']
|
||||
content text [not null]
|
||||
severity text [not null, default: 'medium']
|
||||
tags "text[]" [not null, default: `'{}'`]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
updated_at timestamptz [not null, default: `now()`]
|
||||
}
|
||||
|
||||
Table project_skills {
|
||||
project_id uuid [not null, ref: > projects.guid]
|
||||
skill_id uuid [not null, ref: > agent_skills.id]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(project_id, skill_id) [pk]
|
||||
project_id
|
||||
}
|
||||
}
|
||||
|
||||
Table project_guardrails {
|
||||
project_id uuid [not null, ref: > projects.guid]
|
||||
guardrail_id uuid [not null, ref: > agent_guardrails.id]
|
||||
created_at timestamptz [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
(project_id, guardrail_id) [pk]
|
||||
project_id
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-file refs (for relspecgo merge)
|
||||
Ref: project_skills.project_id > projects.guid [delete: cascade]
|
||||
Ref: project_guardrails.project_id > projects.guid [delete: cascade]
|
||||
Reference in New Issue
Block a user