Skip to content

Commit 0c8c79b

Browse files
feat: stacks, auth store, and strict-provider ReAct (v0.9.0)
Ship model stacks/CLI + Studio apply, auth.json, models catalog, and prime-agent ports. Bump GoLangGraph to v0.2.2 so DeepSeek multi-turn tool calls stay valid; DeepSeek default URL is https://api.deepseek.com. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent fe3b18f commit 0c8c79b

78 files changed

Lines changed: 5396 additions & 539 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ web/tsconfig.tsbuildinfo
1313
.env.local
1414
# Reference checkout used during development; not part of the slmcode release.
1515
little-coder/
16+
prime-agent/

‎Makefile‎

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MODULE := github.com/UnicoLab/slmcode
22
BIN := slmcode
3-
VERSION ?= 0.8.3
3+
VERSION ?= 0.9.0
44
PREFIX ?= $(HOME)/.local
55
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
66
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
@@ -54,16 +54,20 @@ help: ## Show this help
5454

5555
# ── Stack: list available stacks ──
5656
stack-list:
57-
@echo "Available SLMCode stacks:"
58-
@echo ""
59-
@for f in $(STACKS_DIR)/*.yaml; do \
60-
name=$$(basename "$$f" .yaml); \
61-
prov=$$(grep -E '^provider:' "$$f" | head -1 | sed 's/provider: *//'); \
62-
model=$$(grep -E '^model:' "$$f" | head -1 | sed 's/model: *//'); \
63-
printf " %-20s → %-12s %s\n" "$$name" "$$prov" "$$model"; \
64-
done
65-
@echo ""
66-
@echo "Usage: make stack-apply stack=<name>"
57+
@if command -v slmcode >/dev/null 2>&1 || [ -x ./bin/slmcode ]; then \
58+
(./bin/slmcode stack list 2>/dev/null || slmcode stack list); \
59+
else \
60+
echo "Available SLMCode stacks:"; \
61+
echo ""; \
62+
for f in $(STACKS_DIR)/*.yaml; do \
63+
name=$$(basename "$$f" .yaml); \
64+
prov=$$(grep -E '^provider:' "$$f" | head -1 | sed 's/provider: *//'); \
65+
model=$$(grep -E '^model:' "$$f" | head -1 | sed 's/model: *//'); \
66+
printf " %-20s → %-12s %s\n" "$$name" "$$prov" "$$model"; \
67+
done; \
68+
echo ""; \
69+
echo "Usage: slmcode stack apply <name> (or: make stack-apply stack=<name>)"; \
70+
fi
6771

6872
# ── Stack: show a stack config ──
6973
stack-show:
@@ -75,39 +79,25 @@ stack-show:
7579
@echo "═══ Stack: $(stack) ═══"
7680
@cat "$(STACKS_DIR)/$(stack).yaml"
7781

78-
# ── Stack: apply a stack config ──
82+
# ── Stack: apply via slmcode (merge — keeps listen/skills/mcp/api_key) ──
83+
# Optional: agents=1 clear=1 force=1
7984
stack-apply:
80-
@if [ ! -f "$(STACKS_DIR)/$(stack).yaml" ]; then \
81-
echo "Stack '$(stack)' not found. Available:"; \
82-
ls $(STACKS_DIR)/*.yaml 2>/dev/null | xargs -n1 basename | sed 's/.yaml//'; \
85+
@if [ -z "$(stack)" ]; then echo "Usage: make stack-apply stack=<name> [agents=1] [clear=1]"; exit 1; fi
86+
@FLAGS=""; \
87+
if [ "$(agents)" = "1" ]; then FLAGS="$$FLAGS --agents"; fi; \
88+
if [ "$(clear)" = "1" ]; then FLAGS="$$FLAGS --clear-agent-llm"; fi; \
89+
if [ "$(force)" = "1" ]; then FLAGS="$$FLAGS --force-agents"; fi; \
90+
if [ -x ./bin/slmcode ]; then \
91+
./bin/slmcode stack apply $(stack) $$FLAGS; \
92+
elif command -v slmcode >/dev/null 2>&1; then \
93+
slmcode stack apply $(stack) $$FLAGS; \
94+
else \
95+
echo "slmcode binary not found — run: make build && ./bin/slmcode stack apply $(stack)"; \
8396
exit 1; \
8497
fi
85-
@CONFIG_PATH="$${SLMCODE_CONFIG:-$$(pwd)/.slmcode/config.yaml}"; \
86-
if [ -f "$$CONFIG_PATH" ]; then \
87-
echo "⚠ Config already exists at $$CONFIG_PATH"; \
88-
echo " Run 'make stack-apply-force stack=$(stack)' to overwrite"; \
89-
echo " Or 'make stack-new name=my-backup' to save current config first"; \
90-
exit 1; \
91-
fi
92-
@mkdir -p .slmcode
93-
@cp "$(STACKS_DIR)/$(stack).yaml" .slmcode/config.yaml
94-
@echo "✔ Stack '$(stack)' applied → .slmcode/config.yaml"
95-
@echo " Provider: $$(grep '^provider:' .slmcode/config.yaml | sed 's/provider: *//')"
96-
@echo " Model: $$(grep '^model:' .slmcode/config.yaml | sed 's/model: *//')"
97-
@echo ""
98-
@echo " Run your task: slmcode run -v \"your task\""
9998

100-
# ── Stack: force-apply a stack config ──
101-
stack-apply-force:
102-
@if [ ! -f "$(STACKS_DIR)/$(stack).yaml" ]; then \
103-
echo "Stack '$(stack)' not found. Available:"; \
104-
ls $(STACKS_DIR)/*.yaml 2>/dev/null | xargs -n1 basename | sed 's/.yaml//'; \
105-
exit 1; \
106-
fi
107-
@mkdir -p .slmcode
108-
@cp "$(STACKS_DIR)/$(stack).yaml" .slmcode/config.yaml
109-
@echo "✔ Stack '$(stack)' force-applied → .slmcode/config.yaml"
110-
@grep -E '^(provider|model|backend):' .slmcode/config.yaml | sed 's/^/ /'
99+
# ── Stack: apply (same as stack-apply; kept for backwards compat) ──
100+
stack-apply-force: stack-apply
111101

112102
# ── Stack: edit a stack ──
113103
stack-edit:
@@ -171,6 +161,7 @@ test: ## Run unit tests
171161
e2e: ## Run e2e tests (set RUN_E2E=1 for live oMLX tests)
172162
go test ./test/e2e/ -count=1 -timeout 30m
173163
@if command -v node >/dev/null 2>&1; then node cmd/slmcode/ui/markdown_node_test.js; fi
164+
@./scripts/e2e_prime_smoke.sh
174165
@if [ "$$RUN_E2E" = "1" ]; then \
175166
go test ./test/e2e/ -count=1 -timeout 45m -run 'TestLiveOMLX|TestIsolatedMultiAgent'; \
176167
fi

‎README.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ Deep dive: **[docs/PROVIDERS.md](docs/PROVIDERS.md)**
127127
- ⚡ Token-stream early-exit + SLM JSON repair
128128
- 🖥️ Premium TUI + offline Studio GUI (`http://127.0.0.1:7420`)
129129
- 🔐 Shell permission modes: `allow` | `ask` | `deny`
130+
- 📦 **Stacks** — `slmcode stack apply omlx-local|deepseek|…` (global + optional per-agent pins)
131+
- 🔑 **Auth store** — `.slmcode/auth.json` + Studio/TUI `/auth` (keep keys out of config)
132+
- 🔎 **Model catalog** — `find_models` + Studio costs / enabled models
130133

131134
---
132135

@@ -156,6 +159,8 @@ slmcode studio # http://127.0.0.1:7420
156159
Useful knobs:
157160

158161
```bash
162+
slmcode stack list
163+
slmcode stack apply omlx-local # or: deepseek, openai, ollama-local, …
159164
slmcode run --think-passes 2 --parallel 3 --retries 2 "…"
160165
slmcode run --agent explorer "Where is auth handled?"
161166
slmcode run --skill atomic-coding "Refactor helpers"
@@ -169,14 +174,15 @@ slmcode config set dry_run false
169174
| Command | Purpose |
170175
|---------|---------|
171176
| `init` / `doctor` / `config` | Workspace + provider health |
177+
| `stack` / `agent` | Apply model stacks; inspect agent pins |
172178
| `run -v` | Full pipeline + live stream |
173179
| `tui` / bare `slmcode` | Premium interactive TUI |
174180
| `chat` | Classic REPL |
175181
| `board` / `watch` | Colored kanban |
176182
| `studio` | GUI + SSE API |
177183
| `update` | Refresh install (binary or source) |
178184

179-
TUI: `/compact`, `/sessions`, `/stats`, `/permission`, `/agents`, `/stop`, `/resume`.
185+
TUI: `/compact`, `/models`, `/mcp`, `/auth`, `/schema`, `/sessions`, `/stats`, `/permission`, `/agents`, `/stop`, `/resume`.
180186

181187
---
182188

‎cmd/slmcode/cmd_agent.go‎

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/UnicoLab/slmcode/pkg/agents"
11+
"github.com/UnicoLab/slmcode/pkg/cli"
12+
"github.com/UnicoLab/slmcode/pkg/config"
13+
)
14+
15+
func agentCmd() *cobra.Command {
16+
cmd := &cobra.Command{
17+
Use: "agent",
18+
Short: "List / show / edit specialist agents (effective LLM after stack inheritance)",
19+
Long: cli.Dim(`Per-agent provider/model/endpoint overrides live in .slmcode/agents/.
20+
Empty fields inherit the active stack / global config.
21+
22+
Bulk role pins: slmcode stack apply <name> --agents
23+
Clear pins: slmcode stack apply <name> --clear-agent-llm`),
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
return agentList(cmd, args)
26+
},
27+
}
28+
cmd.AddCommand(
29+
&cobra.Command{Use: "list", Aliases: []string{"ls"}, Short: "List agents with effective LLM", RunE: agentList},
30+
&cobra.Command{
31+
Use: "show [id]",
32+
Short: "Show one agent (includes effective_model)",
33+
Args: cobra.ExactArgs(1),
34+
RunE: func(cmd *cobra.Command, args []string) error {
35+
ws, err := openWorkspace()
36+
if err != nil {
37+
return err
38+
}
39+
custom, _ := cli.LoadProjectCustoms(ws.Config.AgentsDir())
40+
a := agents.AgentDetail(args[0], custom)
41+
if a == nil {
42+
return fmt.Errorf("agent %q not found", args[0])
43+
}
44+
enriched := agents.EnrichPublicSpecs(
45+
[]map[string]interface{}{a},
46+
config.NormalizeProvider(ws.Config.Provider),
47+
ws.Config.Model,
48+
ws.Config.ActiveStack,
49+
)
50+
cli.Header("agent:" + args[0])
51+
fmt.Print(cli.FormatAgentShow(enriched[0]))
52+
return nil
53+
},
54+
},
55+
&cobra.Command{
56+
Use: "edit [id] [key=value…]",
57+
Short: "Patch agent fields (model= provider= endpoint= …)",
58+
Args: cobra.MinimumNArgs(1),
59+
RunE: func(cmd *cobra.Command, args []string) error {
60+
ws, err := openWorkspace()
61+
if err != nil {
62+
return err
63+
}
64+
id := strings.ToLower(strings.TrimSpace(args[0]))
65+
fields := map[string]string{}
66+
for _, a := range args[1:] {
67+
k, v, ok := strings.Cut(a, "=")
68+
if !ok {
69+
continue
70+
}
71+
fields[strings.ToLower(strings.TrimSpace(k))] = strings.TrimSpace(v)
72+
}
73+
if len(fields) == 0 {
74+
return fmt.Errorf("usage: slmcode agent edit <id> model=… provider=… endpoint=…")
75+
}
76+
path := filepath.Join(ws.Config.AgentsDir(), id+".yaml")
77+
var base agents.CustomSpec
78+
if got, rerr := agents.ReadCustomFile(path); rerr == nil {
79+
base = got
80+
} else {
81+
base.ID = id
82+
if agents.BuiltinIDs()[id] {
83+
base.Override = true
84+
base.Builtin = true
85+
}
86+
}
87+
applyAgentFields(&base, fields)
88+
if _, err := agents.WriteCustom(ws.Config.AgentsDir(), base); err != nil {
89+
return err
90+
}
91+
cli.Header("updated " + id)
92+
custom, _ := cli.LoadProjectCustoms(ws.Config.AgentsDir())
93+
a := agents.AgentDetail(id, custom)
94+
enriched := agents.EnrichPublicSpecs(
95+
[]map[string]interface{}{a},
96+
config.NormalizeProvider(ws.Config.Provider),
97+
ws.Config.Model,
98+
ws.Config.ActiveStack,
99+
)
100+
fmt.Print(cli.FormatAgentShow(enriched[0]))
101+
fmt.Println(cli.Dim(" Restart studio/tui or rebuild to pick up LLM changes."))
102+
return nil
103+
},
104+
},
105+
&cobra.Command{
106+
Use: "clear-llm [id]",
107+
Short: "Clear model/provider/endpoint so the agent inherits the stack",
108+
Args: cobra.ExactArgs(1),
109+
RunE: func(cmd *cobra.Command, args []string) error {
110+
ws, err := openWorkspace()
111+
if err != nil {
112+
return err
113+
}
114+
id := strings.ToLower(strings.TrimSpace(args[0]))
115+
path := filepath.Join(ws.Config.AgentsDir(), id+".yaml")
116+
got, err := agents.ReadCustomFile(path)
117+
if err != nil {
118+
return fmt.Errorf("no override file for %q — already inheriting", id)
119+
}
120+
got.Model = ""
121+
got.Provider = ""
122+
got.Endpoint = ""
123+
if _, err := agents.WriteCustom(ws.Config.AgentsDir(), got); err != nil {
124+
return err
125+
}
126+
fmt.Println(cli.Success(id + " now inherits stack/global LLM"))
127+
return nil
128+
},
129+
},
130+
)
131+
return cmd
132+
}
133+
134+
func agentList(cmd *cobra.Command, args []string) error {
135+
ws, err := openWorkspace()
136+
if err != nil {
137+
return err
138+
}
139+
custom, _ := cli.LoadProjectCustoms(ws.Config.AgentsDir())
140+
cli.Header("Agents")
141+
if ws.Config.ActiveStack != "" {
142+
cli.KeyVal("active_stack", ws.Config.ActiveStack)
143+
}
144+
cli.KeyVal("global", config.NormalizeProvider(ws.Config.Provider)+"/"+ws.Config.Model)
145+
fmt.Println()
146+
fmt.Print(cli.FormatAgentListWithGlobals(custom, config.NormalizeProvider(ws.Config.Provider), ws.Config.Model))
147+
return nil
148+
}
149+
150+
func applyAgentFields(c *agents.CustomSpec, fields map[string]string) {
151+
for k, v := range fields {
152+
switch k {
153+
case "title":
154+
c.Title = v
155+
case "description":
156+
c.Description = v
157+
case "model":
158+
c.Model = v
159+
case "provider":
160+
c.Provider = v
161+
case "endpoint":
162+
c.Endpoint = v
163+
case "system_prompt", "prompt":
164+
c.SystemPrompt = v
165+
case "skills":
166+
var skills []string
167+
for _, s := range strings.Split(v, ",") {
168+
s = strings.TrimSpace(s)
169+
if s != "" {
170+
skills = append(skills, s)
171+
}
172+
}
173+
c.Skills = skills
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)