Skip to content

Commit dc79c88

Browse files
sgx-labsclaude
andcommitted
Init UX overhaul: provider picker, model auto-pull, dimension fixes
- Add interactive provider choice even when Ollama is detected - Auto-pull Ollama models during init if not already present - Fix snowflake-arctic-embed2 dimensions (768 → 1024) - Fix model choice not persisting across init steps - Delete stale DB on re-init to prevent dimension mismatch - Auto-background `same web` so it doesn't block the terminal - Remove redundant openai-compatible base_url warning - Update MCP setup prompt to "highly recommended" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c96eaba commit dc79c88

9 files changed

Lines changed: 436 additions & 173 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ One-command install of pre-built knowledge vaults. `same seed install claude-cod
229229
- **OpenAI-compatible embedding provider** — SAME now supports any server that exposes the OpenAI-compatible `/v1/embeddings` endpoint. Use `provider = "openai-compatible"` with llama.cpp, VLLM, LM Studio, or any other compatible inference engine. API key is optional for local servers. Configure via `[embedding]` in config or `SAME_EMBED_PROVIDER` / `SAME_EMBED_BASE_URL` / `SAME_EMBED_MODEL` environment variables.
230230
- **Non-localhost security warning** — when using an OpenAI-compatible provider with a remote base URL, SAME prints a warning that embedding requests will leave your machine. Local servers (localhost, 127.0.0.1, ::1) are silent.
231231
- **`--hooks-only` flag for init**`same init --hooks-only` skips MCP setup for Claude Code-only workflows (mirrors existing `--mcp-only` for Cursor/Windsurf).
232-
- **Expanded embedding model support** — auto-detected dimensions for 5 new models: `snowflake-arctic-embed2` (768), `embeddinggemma` (768), `qwen3-embedding` (1024), `nomic-embed-text-v2-moe` (768), `bge-m3` (1024). All recognized in dimension defaults and embedding-only model filter.
232+
- **Expanded embedding model support** — auto-detected dimensions for 5 new models: `snowflake-arctic-embed2` (1024), `embeddinggemma` (768), `qwen3-embedding` (1024), `nomic-embed-text-v2-moe` (768), `bge-m3` (1024). All recognized in dimension defaults and embedding-only model filter.
233233
- **Provider-aware onboarding**`same init` detects `openai`/`openai-compatible` providers and skips the Ollama connectivity check, showing provider/model/endpoint info instead.
234234
- **Pre-release verification gate**`make precheck` runs 9 automated checks (version consistency, build, tests, PII scan, git identity, JSON validation, CLI smoke test).
235235

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ Supported embedding models (auto-detected dimensions):
502502
| Model | Dims | Notes |
503503
|-------|------|-------|
504504
| `nomic-embed-text` | 768 | Default. Great balance of quality and speed |
505-
| `snowflake-arctic-embed2` | 768 | Recommended upgrade. Best retrieval in its size class |
505+
| `snowflake-arctic-embed2` | 1024 | Recommended upgrade. Best retrieval in its size class |
506506
| `mxbai-embed-large` | 1024 | Highest overall MTEB average |
507507
| `all-minilm` | 384 | Lightweight (~90MB). Good for constrained hardware |
508508
| `snowflake-arctic-embed` | 1024 | v1 large model |

cmd/same/web_cmd.go

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"fmt"
6+
"net"
67
"os"
78
"os/exec"
89
"os/signal"
@@ -19,26 +20,40 @@ import (
1920

2021
func webCmd() *cobra.Command {
2122
var (
22-
port int
23-
openFlag bool
23+
port int
24+
openFlag bool
25+
foreground bool
2426
)
2527
cmd := &cobra.Command{
2628
Use: "web",
2729
Short: "Open the vault dashboard in your browser",
2830
Long: `Start a local web server for the vault dashboard.
2931
3032
The dashboard is read-only and only accessible from localhost.
33+
The server runs in the background by default.
3134
3235
Examples:
33-
same web # Start on port 4078
36+
same web # Start background server, open browser
3437
same web --port 8080 # Custom port
35-
same web --open # Auto-open browser`,
38+
same web --fg # Run in foreground (blocks terminal)`,
3639
RunE: func(cmd *cobra.Command, args []string) error {
3740
vp := config.VaultPath()
3841
if vp == "" {
3942
return config.ErrNoVault
4043
}
44+
// Verify the vault actually exists on disk
45+
if _, err := os.Stat(vp); err != nil {
46+
return fmt.Errorf("vault path does not exist: %s", vp)
47+
}
48+
49+
addr := fmt.Sprintf("127.0.0.1:%d", port)
50+
51+
// Background mode: re-exec ourselves as a detached process
52+
if !foreground && os.Getenv("_SAME_WEB_BG") == "" {
53+
return launchBackground(addr, port, vp)
54+
}
4155

56+
// Foreground mode (or background child process)
4257
ctx, cancel := context.WithCancel(cmd.Context())
4358
defer cancel()
4459

@@ -50,33 +65,77 @@ Examples:
5065
select {
5166
case <-ctx.Done():
5267
case <-sigCh:
53-
fmt.Fprintln(os.Stderr, "Shutting down...")
5468
cancel()
5569
}
5670
}()
5771

5872
// Create embed provider (nil is fine — keyword fallback)
5973
embedClient, _ := newEmbedProvider()
6074

61-
addr := fmt.Sprintf("127.0.0.1:%d", port)
62-
fmt.Printf("\n Dashboard: %shttp://%s%s\n", cli.Bold, addr, cli.Reset)
63-
fmt.Printf(" Press Ctrl+C to stop\n\n")
64-
65-
if openFlag {
66-
go func() {
67-
time.Sleep(300 * time.Millisecond)
68-
openBrowser(fmt.Sprintf("http://%s", addr))
69-
}()
75+
if foreground {
76+
fmt.Printf("\n Dashboard: %shttp://%s%s\n", cli.Bold, addr, cli.Reset)
77+
fmt.Printf(" %sPress Ctrl+C to stop%s\n\n", cli.Dim, cli.Reset)
7078
}
7179

7280
return web.Serve(ctx, addr, embedClient, Version, vp)
7381
},
7482
}
7583
cmd.Flags().IntVar(&port, "port", 4078, "Port to listen on")
76-
cmd.Flags().BoolVar(&openFlag, "open", false, "Auto-open browser")
84+
cmd.Flags().BoolVar(&openFlag, "open", false, "Auto-open browser (default in background mode)")
85+
cmd.Flags().BoolVar(&foreground, "fg", false, "Run in foreground (blocks terminal)")
7786
return cmd
7887
}
7988

89+
// launchBackground re-execs `same web --fg` as a detached background process,
90+
// waits for the server to start, opens the browser, and returns.
91+
func launchBackground(addr string, port int, vaultPath string) error {
92+
exe, err := os.Executable()
93+
if err != nil {
94+
return fmt.Errorf("find executable: %w", err)
95+
}
96+
97+
child := exec.Command(exe, "web", "--fg", "--port", fmt.Sprintf("%d", port))
98+
child.Env = append(os.Environ(), "_SAME_WEB_BG=1", "VAULT_PATH="+vaultPath)
99+
child.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
100+
child.Stdout = nil
101+
child.Stderr = nil
102+
103+
if err := child.Start(); err != nil {
104+
return fmt.Errorf("start background server: %w", err)
105+
}
106+
107+
// Wait for server to be ready (up to 3 seconds)
108+
url := fmt.Sprintf("http://%s", addr)
109+
ready := false
110+
for i := 0; i < 30; i++ {
111+
time.Sleep(100 * time.Millisecond)
112+
conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
113+
if err == nil {
114+
conn.Close()
115+
ready = true
116+
break
117+
}
118+
}
119+
120+
if !ready {
121+
fmt.Printf(" %s!%s Server may not have started — check port %d\n", cli.Yellow, cli.Reset, port)
122+
return nil
123+
}
124+
125+
// Write PID file for `same web stop` (future feature)
126+
pidPath := config.DataDir() + "/web.pid"
127+
_ = os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", child.Process.Pid)), 0o600)
128+
129+
fmt.Printf("\n %s✓%s Dashboard running at %s%s%s\n", cli.Green, cli.Reset, cli.Bold, url, cli.Reset)
130+
fmt.Printf(" %sPID %d • Stop with: kill %d%s\n\n", cli.Dim, child.Process.Pid, child.Process.Pid, cli.Reset)
131+
132+
openBrowser(url)
133+
134+
// Detach — don't wait for child
135+
_ = child.Process.Release()
136+
return nil
137+
}
138+
80139
func openBrowser(url string) {
81140
var cmd *exec.Cmd
82141
switch runtime.GOOS {

internal/config/config.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func EmbeddingDim() int {
6262
case "snowflake-arctic-embed":
6363
return 1024
6464
case "snowflake-arctic-embed2":
65-
return 768
65+
return 1024
6666
case "embeddinggemma":
6767
return 768
6868
case "qwen3-embedding":
@@ -88,7 +88,7 @@ type ModelInfo struct {
8888
// KnownModels lists supported embedding models with metadata.
8989
var KnownModels = []ModelInfo{
9090
{"nomic-embed-text", 768, "ollama", "Default. Great balance of quality and speed"},
91-
{"snowflake-arctic-embed2", 768, "ollama", "Best retrieval in its size class"},
91+
{"snowflake-arctic-embed2", 1024, "ollama", "Best retrieval in its size class"},
9292
{"mxbai-embed-large", 1024, "ollama", "Highest overall MTEB average"},
9393
{"all-minilm", 384, "ollama", "Lightweight (~90MB). Good for constrained hardware"},
9494
{"snowflake-arctic-embed", 1024, "ollama", "v1 large model"},
@@ -301,11 +301,6 @@ func LoadConfig() (*Config, error) {
301301
} else if cfg.Memory.CompositeThreshold > 1.0 {
302302
cfg.Memory.CompositeThreshold = 1.0
303303
}
304-
if strings.EqualFold(strings.TrimSpace(cfg.Embedding.Provider), "openai-compatible") &&
305-
strings.TrimSpace(cfg.Embedding.BaseURL) == "" {
306-
fmt.Fprintln(os.Stderr, "same: warning: openai-compatible provider requires base_url (set embedding.base_url or SAME_EMBED_BASE_URL)")
307-
}
308-
309304
// Apply TOML skip_dirs to the global SkipDirs map.
310305
// Previously parsed but never applied — this fixes the bug.
311306
if len(cfg.Vault.SkipDirs) > 0 {
@@ -438,9 +433,16 @@ func generateTOMLContent(vaultPath string) string {
438433
b.WriteString("handoff_dir = \"sessions\"\n")
439434
b.WriteString("decision_log = \"decisions.md\"\n\n")
440435

436+
// Use the active model (may have been changed via model picker or env var)
437+
activeModel := EmbeddingModel
438+
ec := EmbeddingProviderConfig()
439+
if ec.Model != "" {
440+
activeModel = ec.Model
441+
}
442+
441443
b.WriteString("[ollama]\n")
442444
b.WriteString("url = \"http://localhost:11434\"\n")
443-
b.WriteString("model = \"nomic-embed-text\"\n\n")
445+
b.WriteString(fmt.Sprintf("model = %q\n\n", activeModel))
444446

445447
b.WriteString("[embedding]\n")
446448
b.WriteString("# Embedding provider: \"ollama\" (default), \"openai\", \"openai-compatible\", or \"none\" (keyword-only)\n")
@@ -449,7 +451,7 @@ func generateTOMLContent(vaultPath string) string {
449451
activeProvider = "ollama"
450452
}
451453
b.WriteString(fmt.Sprintf("provider = %q\n", activeProvider))
452-
b.WriteString(fmt.Sprintf("model = %q\n", EmbeddingModel))
454+
b.WriteString(fmt.Sprintf("model = %q\n", activeModel))
453455
b.WriteString("# api_key = \"\" # required for cloud providers\n")
454456
b.WriteString("# # or set SAME_EMBED_API_KEY / OPENAI_API_KEY\n")
455457
b.WriteString("# dimensions = 0 # 0 = use provider default\n\n")

internal/config/config_test.go

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package config
22

33
import (
4-
"io"
54
"os"
65
"path/filepath"
76
"runtime"
@@ -389,23 +388,7 @@ func TestLoadConfig_MissingBaseURL_OpenAICompatible(t *testing.T) {
389388
t.Fatalf("write config: %v", err)
390389
}
391390

392-
oldStderr := os.Stderr
393-
r, w, err := os.Pipe()
394-
if err != nil {
395-
t.Fatalf("os.Pipe: %v", err)
396-
}
397-
os.Stderr = w
398-
399391
cfg, err := LoadConfig()
400-
401-
_ = w.Close()
402-
os.Stderr = oldStderr
403-
404-
var warnBuf strings.Builder
405-
if _, copyErr := io.Copy(&warnBuf, r); copyErr != nil {
406-
t.Fatalf("io.Copy stderr: %v", copyErr)
407-
}
408-
409392
if err != nil {
410393
t.Fatalf("LoadConfig: %v", err)
411394
}
@@ -416,9 +399,6 @@ func TestLoadConfig_MissingBaseURL_OpenAICompatible(t *testing.T) {
416399
if ec.BaseURL != "" {
417400
t.Fatalf("expected empty base URL, got %q", ec.BaseURL)
418401
}
419-
if !strings.Contains(warnBuf.String(), "openai-compatible provider requires base_url") {
420-
t.Fatalf("expected base_url warning, got: %q", warnBuf.String())
421-
}
422402
}
423403

424404
func TestLoadConfig_ZeroDimensionsFallsBackToModelDefault(t *testing.T) {

internal/embedding/ollama.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ func ollamaDefaultDims(model string) int {
251251
case "snowflake-arctic-embed":
252252
return 1024
253253
case "snowflake-arctic-embed2":
254-
return 768
254+
return 1024
255255
case "embeddinggemma":
256256
return 768
257257
case "qwen3-embedding":

internal/embedding/ollama_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ func TestOllamaDefaultDims(t *testing.T) {
276276
{"mxbai-embed-large", 1024},
277277
{"all-minilm", 384},
278278
{"snowflake-arctic-embed", 1024},
279-
{"snowflake-arctic-embed2", 768},
279+
{"snowflake-arctic-embed2", 1024},
280280
{"embeddinggemma", 768},
281281
{"qwen3-embedding", 1024},
282282
{"nomic-embed-text-v2-moe", 768},

0 commit comments

Comments
 (0)