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
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ This means `flare-edge-cli` is not just a deploy wrapper. It is an agent-oriente
- Validates Go code against a Workers/Wasm compatibility profile
- Builds `.wasm` artifacts and the JavaScript Worker shim
- Runs local or remote dev sessions through Wrangler
- Configures Workers AI bindings for Go-based AI Workers
- Deploys versioned Workers and manages routes, secrets, KV, D1, R2, and releases
- Tears down Workers and optional side-effect resources cleanly

Expand Down Expand Up @@ -235,10 +236,42 @@ Supported templates:
- `edge-http`
- `edge-json`
- `scheduled`
- `ai-text`
- `ai-chat`
- `ai-vision`
- `ai-stt`
- `ai-tts`
- `ai-image`
- `ai-embeddings`
- `kv-api`
- `d1-api`
- `r2-api`

AI templates:

- `ai-text`: backward-compatible simple text inference scaffold, now using `@cf/moonshotai/kimi-k2.5`
- `ai-chat`: chat-style text generation scaffold using `@cf/moonshotai/kimi-k2.5`
- `ai-vision`: vision scaffold using `@cf/moonshotai/kimi-k2.5`
- `ai-stt`: speech-to-text scaffold using `@cf/deepgram/nova-3`
- `ai-tts`: text-to-speech scaffold using `@cf/deepgram/aura-2-en`
- `ai-image`: text-to-image scaffold using `@cf/black-forest-labs/flux-2-klein-9b`
- `ai-embeddings`: embeddings scaffold using `@cf/qwen/qwen3-embedding-0.6b`

### `ai`

Manage the Workers AI binding stored in local project config.

```bash
./flare-edge-cli ai binding set [--path <dir>] [--binding <name>] [--remote] [--json]
./flare-edge-cli ai binding clear [--path <dir>] [--json]
```

Notes:

- `ai binding set` updates both `flare-edge.json` and `wrangler.jsonc`
- the default binding name is `AI`
- `--remote` should remain enabled for Workers AI development

### `compat`

Run the Go/Wasm compatibility analyzer or inspect built-in rules.
Expand Down Expand Up @@ -427,6 +460,30 @@ Scaffold a JSON worker:
./flare-edge-cli init test-project --module-path github.com/example/test-project --template edge-json
```

Scaffold a Go-based AI Worker:

```bash
./flare-edge-cli init ai-worker --module-path github.com/example/ai-worker --template ai-text
```

Scaffold a vision Worker:

```bash
./flare-edge-cli init ai-vision-worker --module-path github.com/example/ai-vision-worker --template ai-vision
```

Scaffold a speech-to-text Worker:

```bash
./flare-edge-cli init ai-stt-worker --module-path github.com/example/ai-stt-worker --template ai-stt
```

Add Workers AI to an existing project:

```bash
./flare-edge-cli ai binding set --path ./ai-worker --binding AI --remote
```

Check compatibility and emit JSON:

```bash
Expand Down
2 changes: 2 additions & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/paolo/flare-edge-cli/internal/infra/toolchain"
"github.com/paolo/flare-edge-cli/internal/infra/wrangler"
"github.com/paolo/flare-edge-cli/internal/logging"
aisvc "github.com/paolo/flare-edge-cli/internal/service/ai"
authsvc "github.com/paolo/flare-edge-cli/internal/service/auth"
buildsvc "github.com/paolo/flare-edge-cli/internal/service/build"
compatsvc "github.com/paolo/flare-edge-cli/internal/service/compat"
Expand Down Expand Up @@ -43,6 +44,7 @@ func Run() int {

deps := cli.Dependencies{
Services: cli.Services{
AI: aisvc.NewService(store, filesystem),
Auth: authsvc.NewService(wranglerClient, authState),
Build: buildService,
Compat: compatsvc.NewService(),
Expand Down
58 changes: 58 additions & 0 deletions internal/cli/ai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package cli

import (
"context"

"github.com/spf13/cobra"

"github.com/paolo/flare-edge-cli/internal/output"
aisvc "github.com/paolo/flare-edge-cli/internal/service/ai"
)

func newAICommand(deps Dependencies) *cobra.Command {
cmd := &cobra.Command{Use: "ai", Short: "Manage Workers AI bindings"}
binding := &cobra.Command{Use: "binding", Short: "Manage Workers AI binding config"}
binding.AddCommand(newAIBindingSetCommand(deps), newAIBindingClearCommand(deps))
cmd.AddCommand(binding)
return cmd
}

func newAIBindingSetCommand(deps Dependencies) *cobra.Command {
var options aisvc.SetBindingOptions
var jsonOutput bool
cmd := &cobra.Command{
Use: "set",
Short: "Configure the Workers AI binding",
RunE: func(cmd *cobra.Command, _ []string) error {
result, err := deps.Services.AI.SetBinding(context.Background(), options)
if err != nil {
return err
}
return output.NewPrinter(cmd.OutOrStdout(), cmd.ErrOrStderr(), jsonOutput).Print(result)
},
}
cmd.Flags().StringVar(&options.Dir, "path", ".", "Project path")
cmd.Flags().StringVar(&options.Binding, "binding", "AI", "Workers AI binding name")
cmd.Flags().BoolVar(&options.Remote, "remote", true, "Use the remote Workers AI binding")
cmd.Flags().BoolVar(&jsonOutput, "json", false, "Emit machine-readable JSON")
return cmd
}

func newAIBindingClearCommand(deps Dependencies) *cobra.Command {
var options aisvc.ClearBindingOptions
var jsonOutput bool
cmd := &cobra.Command{
Use: "clear",
Short: "Remove the Workers AI binding from local config",
RunE: func(cmd *cobra.Command, _ []string) error {
result, err := deps.Services.AI.ClearBinding(context.Background(), options)
if err != nil {
return err
}
return output.NewPrinter(cmd.OutOrStdout(), cmd.ErrOrStderr(), jsonOutput).Print(result)
},
}
cmd.Flags().StringVar(&options.Dir, "path", ".", "Project path")
cmd.Flags().BoolVar(&jsonOutput, "json", false, "Emit machine-readable JSON")
return cmd
}
2 changes: 1 addition & 1 deletion internal/cli/aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func newInitAliasCommand(deps Dependencies) *cobra.Command {
cmd.Flags().StringVar(&options.Dir, "cwd", ".", "Target parent directory")
cmd.Flags().StringVar(&options.ModulePath, "module-path", "", "Go module path")
cmd.Flags().StringVar(&options.PackageName, "package", "", "Package name")
cmd.Flags().StringVar(&options.Template, "template", "edge-http", "Starter template")
cmd.Flags().StringVar(&options.Template, "template", "edge-http", "Starter template: edge-http|edge-json|scheduled|kv-api|d1-api|r2-api|ai-text|ai-chat|ai-vision|ai-stt|ai-tts|ai-image|ai-embeddings")
cmd.Flags().StringVar(&options.CompatDate, "compat-date", "", "Compatibility date")
cmd.Flags().StringVar(&options.Env, "env", "", "Wrangler environment")
cmd.Flags().BoolVar(&options.UseJSONC, "use-jsonc", false, "Generate wrangler.jsonc")
Expand Down
2 changes: 2 additions & 0 deletions internal/cli/deps.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
aisvc "github.com/paolo/flare-edge-cli/internal/service/ai"
authsvc "github.com/paolo/flare-edge-cli/internal/service/auth"
buildsvc "github.com/paolo/flare-edge-cli/internal/service/build"
compatsvc "github.com/paolo/flare-edge-cli/internal/service/compat"
Expand All @@ -19,6 +20,7 @@ import (
)

type Services struct {
AI *aisvc.Service
Auth *authsvc.Service
Build *buildsvc.Service
Compat *compatsvc.Service
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func newProjectInitCommand(deps Dependencies) *cobra.Command {

cmd.Flags().StringVar(&options.ModulePath, "module-path", "", "Go module path to initialize")
cmd.Flags().StringVar(&options.PackageName, "package", "", "Package name for generated Go entrypoint")
cmd.Flags().StringVar(&options.Template, "template", "edge-http", "Starter template: edge-http|edge-json|scheduled|kv-api|d1-api|r2-api")
cmd.Flags().StringVar(&options.Template, "template", "edge-http", "Starter template: edge-http|edge-json|scheduled|kv-api|d1-api|r2-api|ai-text|ai-chat|ai-vision|ai-stt|ai-tts|ai-image|ai-embeddings")
cmd.Flags().StringVar(&options.CompatDate, "compat-date", "", "Cloudflare compatibility date")
cmd.Flags().StringVar(&options.Env, "env", "", "Default Wrangler environment")
cmd.Flags().BoolVar(&options.UseJSONC, "use-jsonc", false, "Generate wrangler.jsonc output")
Expand Down
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func NewRootCommand(deps Dependencies) *cobra.Command {
}
cmd.PersistentFlags().StringVar(&accountID, "account-id", "", "Cloudflare account ID override for all commands")

cmd.AddCommand(newAICommand(deps))
cmd.AddCommand(newAuthCommand(deps))
cmd.AddCommand(newBuildCommand(deps))
cmd.AddCommand(newCompatCommand(deps))
Expand Down
21 changes: 20 additions & 1 deletion internal/domain/config/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Project struct {
type ProjectBindings struct {
Vars map[string]string `json:"vars,omitempty"`
Secrets []string `json:"secrets,omitempty"`
AI *AIBinding `json:"ai,omitempty"`
KV []KVBinding `json:"kv,omitempty"`
D1 []D1Binding `json:"d1,omitempty"`
R2 []R2Binding `json:"r2,omitempty"`
Expand All @@ -47,6 +48,11 @@ type Environment struct {
Bindings ProjectBindings `json:"bindings"`
}

type AIBinding struct {
Binding string `json:"binding"`
Remote bool `json:"remote,omitempty"`
}

type KVBinding struct {
Binding string `json:"binding"`
ID string `json:"id,omitempty"`
Expand Down Expand Up @@ -74,7 +80,7 @@ func DefaultProject(name, modulePath, packageName, template, compatDate, env str
}

entry := "./cmd/worker"
return Project{
project := Project{
SchemaVersion: 1,
ProjectName: name,
ModulePath: modulePath,
Expand All @@ -96,8 +102,21 @@ func DefaultProject(name, modulePath, packageName, template, compatDate, env str
WasmExecSource: "internal/generated/wasm_exec.js",
},
}
if UsesAIBinding(template) {
project.Bindings.AI = &AIBinding{Binding: "AI", Remote: true}
}
return project
}

func (p Project) ArtifactPath() string {
return fmt.Sprintf("%s/%s", p.OutDir, p.WasmFile)
}

func UsesAIBinding(template string) bool {
switch template {
case "ai-text", "ai-chat", "ai-vision", "ai-stt", "ai-tts", "ai-image", "ai-embeddings":
return true
default:
return false
}
}
7 changes: 7 additions & 0 deletions internal/domain/config/wrangler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type WranglerConfig struct {
Main string `json:"main"`
CompatibilityDate string `json:"compatibility_date"`
Observability *WranglerObservability `json:"observability,omitempty"`
AI *WranglerAIBinding `json:"ai,omitempty"`
Vars map[string]string `json:"vars,omitempty"`
KVNamespaces []WranglerKVNamespace `json:"kv_namespaces,omitempty"`
D1Databases []WranglerD1Database `json:"d1_databases,omitempty"`
Expand All @@ -19,13 +20,19 @@ type WranglerObservability struct {

type WranglerEnvConfig struct {
Name string `json:"name,omitempty"`
AI *WranglerAIBinding `json:"ai,omitempty"`
Vars map[string]string `json:"vars,omitempty"`
KVNamespaces []WranglerKVNamespace `json:"kv_namespaces,omitempty"`
D1Databases []WranglerD1Database `json:"d1_databases,omitempty"`
R2Buckets []WranglerR2Bucket `json:"r2_buckets,omitempty"`
Routes []WranglerRoute `json:"routes,omitempty"`
}

type WranglerAIBinding struct {
Binding string `json:"binding"`
Remote bool `json:"remote,omitempty"`
}

type WranglerKVNamespace struct {
Binding string `json:"binding"`
ID string `json:"id,omitempty"`
Expand Down
93 changes: 93 additions & 0 deletions internal/service/ai/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package ai

import (
"context"

"github.com/paolo/flare-edge-cli/internal/domain/config"
"github.com/paolo/flare-edge-cli/internal/infra/configstore"
"github.com/paolo/flare-edge-cli/internal/support/fs"
)

type Service struct {
store *configstore.Store
fs *fs.FileSystem
}

type SetBindingOptions struct {
Dir string
Binding string
Remote bool
}

type ClearBindingOptions struct {
Dir string
}

type BindingResult struct {
Binding string `json:"binding,omitempty"`
Remote bool `json:"remote,omitempty"`
Cleared bool `json:"cleared,omitempty"`
}

func NewService(store *configstore.Store, fs *fs.FileSystem) *Service {
return &Service{store: store, fs: fs}
}

func (s *Service) SetBinding(_ context.Context, options SetBindingOptions) (BindingResult, error) {
project, wranglerCfg, err := loadProjectAndWrangler(options.Dir, s.store)
if err != nil {
return BindingResult{}, err
}

project.Bindings.AI = &config.AIBinding{
Binding: options.Binding,
Remote: options.Remote,
}
wranglerCfg.AI = &config.WranglerAIBinding{
Binding: options.Binding,
Remote: options.Remote,
}

if err := s.store.SaveProject(options.Dir, project); err != nil {
return BindingResult{}, err
}
if err := s.store.SaveWrangler(options.Dir, project.WranglerConfig, wranglerCfg); err != nil {
return BindingResult{}, err
}

return BindingResult{
Binding: options.Binding,
Remote: options.Remote,
}, nil
}

func (s *Service) ClearBinding(_ context.Context, options ClearBindingOptions) (BindingResult, error) {
project, wranglerCfg, err := loadProjectAndWrangler(options.Dir, s.store)
if err != nil {
return BindingResult{}, err
}

project.Bindings.AI = nil
wranglerCfg.AI = nil

if err := s.store.SaveProject(options.Dir, project); err != nil {
return BindingResult{}, err
}
if err := s.store.SaveWrangler(options.Dir, project.WranglerConfig, wranglerCfg); err != nil {
return BindingResult{}, err
}

return BindingResult{Cleared: true}, nil
}

func loadProjectAndWrangler(dir string, store *configstore.Store) (config.Project, config.WranglerConfig, error) {
project, err := store.LoadProject(dir)
if err != nil {
return config.Project{}, config.WranglerConfig{}, err
}
wranglerCfg, err := store.LoadWrangler(dir, project.WranglerConfig)
if err != nil {
return config.Project{}, config.WranglerConfig{}, err
}
return project, wranglerCfg, nil
}
Loading
Loading