Skip to content

Commit c15e53f

Browse files
Add executionModel feature and refactor CLI into modular commands (#150)
* Add executionModel feature and refactor CLI into modular commands - Add executionModel config option to memory plugin for specifying AI provider/model - Refactor monolithic CLI into modular commands: export, import, list, stats, cleanup - Add model selector UI with connected providers and recent models - Add showClear prop to Combobox component - Update documentation and bump version to 0.0.10 * Update execution model placeholder text * Update docs with new CLI commands and ocm-mem binary usage * Bump version to v0.9.06 * Add executionModel field to default config
1 parent e5b2cab commit c15e53f

22 files changed

Lines changed: 1716 additions & 780 deletions

File tree

docs/features/memory.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ The file is only created if it does not already exist. The config is validated o
6161
"inlinePlanning": true,
6262
"maxContextTokens": 4000,
6363
"snapshotToKV": true
64-
}
64+
},
65+
"executionModel": ""
6566
}
6667
```
6768

@@ -105,6 +106,7 @@ Set `baseUrl` to point at any OpenAI-compatible self-hosted service (vLLM, Ollam
105106
| `compaction.inlinePlanning` | Include planning state in compaction context | `true` |
106107
| `compaction.maxContextTokens` | Max tokens for injected memory context | `4000` |
107108
| `compaction.snapshotToKV` | Save pre-compaction snapshot for recovery | `true` |
109+
| `executionModel` | Model override for plan execution sessions (`provider/model`). Falls back to OpenCode's default model. ||
108110

109111
---
110112

@@ -364,6 +366,8 @@ Create a new Code session and send an implementation plan as the first prompt. D
364366

365367
Saves planning state (objective, phases, findings) for the Architect session, creates a new session via the OpenCode API, then sends the plan as the first message to the Code agent. Returns the session ID and title. Only the Architect agent has access to this tool — it is excluded from Code and Memory agents.
366368

369+
The model used for the new Code session is determined by `executionModel` in the plugin config (format: `provider/model`, e.g. `anthropic/claude-sonnet-4-20250514`). If not set, OpenCode's default model resolution is used — typically the `model` field from `opencode.json`.
370+
367371
---
368372

369373
## Planning State
@@ -564,6 +568,68 @@ The cleanup function is idempotent — calling it multiple times is safe.
564568

565569
---
566570

571+
## CLI
572+
573+
The plugin includes the `ocm-mem` CLI for managing memories outside of OpenCode sessions. The CLI auto-detects the project ID from git and resolves the database path automatically.
574+
575+
```bash
576+
ocm-mem <command> [options]
577+
```
578+
579+
### Global Options
580+
581+
| Flag | Description |
582+
|------|-------------|
583+
| `--db-path <path>` | Path to memory database |
584+
| `--project, -p <name>` | Project name or SHA (auto-detected from git) |
585+
| `--dir, -d <path>` | Git repo path for project detection |
586+
| `--help, -h` | Show help |
587+
588+
### Commands
589+
590+
| Command | Description |
591+
|---------|-------------|
592+
| `export` | Export memories to file (JSON or Markdown) |
593+
| `import` | Import memories from file |
594+
| `list` | List projects with memory and session state counts |
595+
| `stats` | Show memory statistics for a project |
596+
| `cleanup` | Delete memories or session states by criteria |
597+
598+
### Usage Examples
599+
600+
```bash
601+
# Export all memories as markdown
602+
ocm-mem export --format markdown --output memories.md
603+
604+
# Export filtered by scope
605+
ocm-mem export --project my-project --scope convention
606+
607+
# Import from JSON
608+
ocm-mem import memories.json --project my-project
609+
610+
# Import from Markdown, skip duplicate detection
611+
ocm-mem import memories.md --project my-project --force
612+
613+
# List all projects
614+
ocm-mem list
615+
616+
# Show stats for current project
617+
ocm-mem stats
618+
619+
# Preview cleanup of old memories (dry run)
620+
ocm-mem cleanup --older-than 90 --dry-run
621+
622+
# Delete specific memories
623+
ocm-mem cleanup --ids 1,2,3 --force
624+
625+
# Clean up expired session states
626+
ocm-mem cleanup --sessions --older-than 30
627+
```
628+
629+
Run `ocm-mem <command> --help` for full options on each command.
630+
631+
---
632+
567633
## Troubleshooting
568634

569635
### Plugin shows "degraded" status

frontend/src/components/settings/MemoryPluginConfig.tsx

Lines changed: 137 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { useState, useEffect } from 'react'
1+
import { useState, useEffect, useMemo } from 'react'
22
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
33
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
44
import { Input } from '@/components/ui/input'
55
import { Button } from '@/components/ui/button'
6+
import { Combobox } from '@/components/ui/combobox'
7+
import type { ComboboxOption } from '@/components/ui/combobox'
8+
import { getProviders } from '@/api/providers'
9+
import { useModelStore } from '@/stores/modelStore'
610
import { Label } from '@/components/ui/label'
711
import { Switch } from '@/components/ui/switch'
8-
import { ChevronDown, ChevronRight, Save, Loader2, Database, Brain, AlertCircle, RefreshCw, Play } from 'lucide-react'
12+
import { ChevronDown, ChevronRight, Save, Loader2, Database, Brain, AlertCircle, RefreshCw, Play, Cpu } from 'lucide-react'
913
import { getPluginConfig, updatePluginConfig, reindexMemories, testEmbeddingConfig } from '@/api/memory'
1014
import { FetchError } from '@/api/fetchWrapper'
1115
import { settingsApi } from '@/api/settings'
@@ -41,6 +45,54 @@ export function MemoryPluginConfig({ memoryPluginEnabled, onToggle }: MemoryPlug
4145
enabled: memoryPluginEnabled && expanded,
4246
})
4347

48+
const { data: providersData } = useQuery({
49+
queryKey: ['providers-for-execution-model'],
50+
queryFn: getProviders,
51+
staleTime: 60000,
52+
enabled: memoryPluginEnabled && expanded,
53+
})
54+
55+
const recentModels = useModelStore((s) => s.recentModels)
56+
57+
const executionModelOptions = useMemo<ComboboxOption[]>(() => {
58+
if (!providersData?.providers) return []
59+
60+
const connectedProviders = providersData.providers.filter((p) => p.isConnected)
61+
62+
const recentSet = new Set<string>()
63+
const recentOptions: ComboboxOption[] = []
64+
65+
for (const recent of recentModels) {
66+
const modelValue = `${recent.providerID}/${recent.modelID}`
67+
const provider = connectedProviders.find((p) => p.id === recent.providerID)
68+
const model = provider?.models[recent.modelID]
69+
if (!model || !provider) continue
70+
recentSet.add(modelValue)
71+
recentOptions.push({
72+
value: modelValue,
73+
label: model.name || model.id,
74+
description: provider.name,
75+
group: 'Recent',
76+
})
77+
}
78+
79+
const providerOptions: ComboboxOption[] = []
80+
for (const provider of connectedProviders.sort((a, b) => a.name.localeCompare(b.name))) {
81+
for (const [modelId, model] of Object.entries(provider.models)) {
82+
const modelValue = `${provider.id}/${modelId}`
83+
if (recentSet.has(modelValue)) continue
84+
providerOptions.push({
85+
value: modelValue,
86+
label: model.name || model.id,
87+
description: modelId,
88+
group: provider.name,
89+
})
90+
}
91+
}
92+
93+
return [...recentOptions, ...providerOptions]
94+
}, [providersData, recentModels])
95+
4496
const config = data?.config
4597
const [localConfig, setLocalConfig] = useState<PluginConfig | null>(null)
4698

@@ -66,12 +118,8 @@ export function MemoryPluginConfig({ memoryPluginEnabled, onToggle }: MemoryPlug
66118
const updateMutation = useMutation({
67119
mutationFn: updatePluginConfig,
68120
onSuccess: (data) => {
69-
showToast.success('Memory plugin configuration saved')
70121
queryClient.setQueryData(['memory-plugin-config'], { config: data.config })
71122
},
72-
onError: () => {
73-
showToast.error('Failed to save configuration')
74-
},
75123
})
76124

77125
const reindexMutation = useMutation({
@@ -112,16 +160,20 @@ export function MemoryPluginConfig({ memoryPluginEnabled, onToggle }: MemoryPlug
112160

113161
const handleSave = async () => {
114162
if (!localConfig) return
163+
showToast.loading('Saving configuration...', { id: 'memory-save' })
115164
updateMutation.mutate(localConfig, {
116165
onSuccess: async () => {
117-
showToast.loading('Restarting OpenCode server...', { id: 'memory-restart' })
166+
showToast.loading('Restarting OpenCode server...', { id: 'memory-save' })
118167
try {
119168
await settingsApi.restartOpenCodeServer()
120-
showToast.success('Configuration saved and server restarted', { id: 'memory-restart' })
169+
showToast.success('Configuration saved and server restarted', { id: 'memory-save' })
121170
} catch {
122-
showToast.error('Failed to restart server', { id: 'memory-restart' })
171+
showToast.error('Failed to restart server', { id: 'memory-save' })
123172
}
124173
},
174+
onError: () => {
175+
showToast.error('Failed to save configuration', { id: 'memory-save' })
176+
},
125177
})
126178
}
127179

@@ -262,40 +314,93 @@ export function MemoryPluginConfig({ memoryPluginEnabled, onToggle }: MemoryPlug
262314
</div>
263315
</>
264316
)}
317+
318+
<div className="space-y-2">
319+
<div className="flex items-center gap-2">
320+
<RefreshCw className="h-4 w-4 text-purple-500" />
321+
<span className="text-sm font-medium">Reindex</span>
322+
</div>
323+
<Button
324+
variant="outline"
325+
size="sm"
326+
onClick={handleReindex}
327+
disabled={reindexMutation.isPending}
328+
className="w-full justify-start"
329+
>
330+
{reindexMutation.isPending ? (
331+
<Loader2 className="h-4 w-4 animate-spin mr-2" />
332+
) : (
333+
<RefreshCw className="h-4 w-4 mr-2" />
334+
)}
335+
Reindex
336+
</Button>
337+
<p className="text-xs text-muted-foreground">
338+
Regenerate embeddings for all memories
339+
</p>
340+
</div>
265341
</div>
266342
</div>
267343

268-
<div className="space-y-4 border-t pt-4">
269-
<div className="flex items-center gap-2 mb-3">
270-
<Database className="h-4 w-4 text-green-500" />
271-
<span className="text-sm font-medium">Storage</span>
344+
<div className="grid gap-4 md:grid-cols-2 border-t pt-4">
345+
<div className="space-y-4">
346+
<div className="flex items-center gap-2 mb-3">
347+
<Database className="h-4 w-4 text-green-500" />
348+
<span className="text-sm font-medium">Storage</span>
349+
</div>
350+
351+
<div className="space-y-2">
352+
<Label htmlFor="dedupThreshold">Deduplication Threshold</Label>
353+
<div className="flex items-center gap-4">
354+
<input
355+
id="dedupThreshold"
356+
type="range"
357+
min="0"
358+
max="0.4"
359+
step="0.05"
360+
value={displayConfig.dedupThreshold ?? 0.25}
361+
onChange={(e) => {
362+
setLocalConfig({
363+
...displayConfig,
364+
dedupThreshold: parseFloat(e.target.value),
365+
})
366+
}}
367+
className="flex-1"
368+
/>
369+
<span className="text-sm text-muted-foreground w-12">
370+
{(displayConfig.dedupThreshold ?? 0.25).toFixed(2)}
371+
</span>
372+
</div>
373+
<p className="text-xs text-muted-foreground">
374+
Lower values = more aggressive deduplication (0.0 - 0.4)
375+
</p>
376+
</div>
272377
</div>
273378

274-
<div className="space-y-2">
275-
<Label htmlFor="dedupThreshold">Deduplication Threshold</Label>
276-
<div className="flex items-center gap-4">
277-
<input
278-
id="dedupThreshold"
279-
type="range"
280-
min="0"
281-
max="0.4"
282-
step="0.05"
283-
value={displayConfig.dedupThreshold ?? 0.25}
284-
onChange={(e) => {
379+
<div className="space-y-4">
380+
<div className="flex items-center gap-2 mb-3">
381+
<Cpu className="h-4 w-4 text-orange-500" />
382+
<span className="text-sm font-medium">Plan Execution</span>
383+
</div>
384+
385+
<div className="space-y-2">
386+
<Label htmlFor="executionModel">Execution Model</Label>
387+
<Combobox
388+
value={displayConfig.executionModel ?? ''}
389+
onChange={(value) => {
285390
setLocalConfig({
286391
...displayConfig,
287-
dedupThreshold: parseFloat(e.target.value),
392+
executionModel: value || undefined,
288393
})
289394
}}
290-
className="flex-1"
395+
options={executionModelOptions}
396+
placeholder="default model"
397+
allowCustomValue
398+
showClear
291399
/>
292-
<span className="text-sm text-muted-foreground w-12">
293-
{(displayConfig.dedupThreshold ?? 0.25).toFixed(2)}
294-
</span>
400+
<p className="text-xs text-muted-foreground">
401+
Model used when executing plans from the Architect. Format: provider/model. Leave empty to use the current session's model.
402+
</p>
295403
</div>
296-
<p className="text-xs text-muted-foreground">
297-
Lower values = more aggressive deduplication (0.0 - 0.4)
298-
</p>
299404
</div>
300405
</div>
301406

@@ -306,31 +411,6 @@ export function MemoryPluginConfig({ memoryPluginEnabled, onToggle }: MemoryPlug
306411
</div>
307412
)}
308413

309-
<div className="space-y-4 border-t pt-4">
310-
<div className="flex items-center justify-between">
311-
<div className="flex items-center gap-2">
312-
<RefreshCw className="h-4 w-4 text-purple-500" />
313-
<span className="text-sm font-medium">Reindex</span>
314-
</div>
315-
<Button
316-
variant="outline"
317-
size="sm"
318-
onClick={handleReindex}
319-
disabled={reindexMutation.isPending}
320-
>
321-
{reindexMutation.isPending ? (
322-
<Loader2 className="h-4 w-4 animate-spin mr-1" />
323-
) : (
324-
<RefreshCw className="h-4 w-4 mr-1" />
325-
)}
326-
Reindex
327-
</Button>
328-
</div>
329-
<p className="text-xs text-muted-foreground">
330-
Regenerate embeddings for all memories. Use when changing embedding model or if embeddings are missing.
331-
</p>
332-
</div>
333-
334414
<div className="flex items-center justify-between pt-2">
335415
<div className="flex items-center gap-2 text-xs text-muted-foreground">
336416
<AlertCircle className="h-3 w-3" />

0 commit comments

Comments
 (0)