ollama source for Momentry Core verification
This commit is contained in:
888
cmd/launch/claude_desktop.go
Normal file
888
cmd/launch/claude_desktop.go
Normal file
@@ -0,0 +1,888 @@
|
||||
package launch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ollama/ollama/cmd/config"
|
||||
"github.com/ollama/ollama/cmd/internal/fileutil"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
claudeDesktopIntegrationName = "claude-desktop"
|
||||
claudeDesktopProfileName = "Ollama"
|
||||
claudeDesktopProfileID = "00000000-0000-4000-8000-000000000114"
|
||||
claudeDesktopGatewayBaseURL = "https://ollama.com"
|
||||
claudeDesktopAPIKeyURL = "https://ollama.com/settings/keys"
|
||||
claudeDesktopModelLabel = "Ollama Cloud"
|
||||
claudeDesktopUnsupported = "Claude Desktop is no longer supported. Existing installations can be restored with 'ollama launch claude-desktop --restore'."
|
||||
claudeDesktopSuccessMessage = "Claude Desktop profile changed to Ollama Cloud."
|
||||
claudeDesktopRestoreMessage = "To restore the usual Claude profile, run: ollama launch claude-desktop --restore"
|
||||
claudeDesktopRestoredMessage = "Claude Desktop restored to the usual Claude profile."
|
||||
)
|
||||
|
||||
var (
|
||||
claudeDesktopGOOS = runtime.GOOS
|
||||
claudeDesktopUserHome = os.UserHomeDir
|
||||
claudeDesktopStat = os.Stat
|
||||
claudeDesktopOpenApp = defaultClaudeDesktopOpenApp
|
||||
claudeDesktopOpenAppPath = defaultClaudeDesktopOpenAppPath
|
||||
claudeDesktopQuitApp = defaultClaudeDesktopQuitApp
|
||||
claudeDesktopIsRunning = defaultClaudeDesktopIsRunning
|
||||
claudeDesktopRunningAppPath = defaultClaudeDesktopRunningAppPath
|
||||
claudeDesktopGlob = filepath.Glob
|
||||
claudeDesktopSleep = time.Sleep
|
||||
claudeDesktopHTTPClient = http.DefaultClient
|
||||
claudeDesktopPromptAPIKey = promptClaudeDesktopAPIKey
|
||||
claudeDesktopValidateAPIKey = validateClaudeDesktopAPIKey
|
||||
)
|
||||
|
||||
// ClaudeDesktop configures and launches Claude Desktop in third-party
|
||||
// inference mode using Ollama Cloud as the gateway.
|
||||
type ClaudeDesktop struct{}
|
||||
|
||||
func (c *ClaudeDesktop) String() string { return "Claude Desktop" }
|
||||
|
||||
func (c *ClaudeDesktop) Supported() error { return claudeDesktopSupported() }
|
||||
|
||||
func (c *ClaudeDesktop) Paths() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) AutodiscoveredModel() string {
|
||||
return claudeDesktopModelLabel
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) ConfigureAutodiscovery() error {
|
||||
if err := claudeDesktopSupported(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targets, err := claudeDesktopTargetPaths()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := claudeDesktopValidatedAPIKey(context.Background(), claudeDesktopTargetProfilePaths(targets))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, path := range targets.normalConfigs {
|
||||
if err := writeClaudeDesktopDeploymentMode(path, "3p"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, target := range targets.thirdPartyProfiles {
|
||||
if err := writeClaudeDesktopDeploymentMode(target.desktopConfig, "3p"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeClaudeDesktopMeta(target.meta, claudeDesktopProfileID, claudeDesktopProfileName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeClaudeDesktopGatewayProfile(target.profile, key, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) RestoreHint() string {
|
||||
return claudeDesktopRestoreMessage
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) ConfigurationSuccessMessage() string {
|
||||
return claudeDesktopSuccessMessage + "\n" + claudeDesktopRestoreMessage
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) RestoreSuccessMessage() string {
|
||||
return claudeDesktopRestoredMessage
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) AutodiscoveryConfigured() bool {
|
||||
targets, err := claudeDesktopTargetPaths()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return claudeDesktopTargetsConfigured(targets)
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) Onboard() error {
|
||||
return config.MarkIntegrationOnboarded(claudeDesktopIntegrationName)
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) RequiresInteractiveOnboarding() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) SkipModelReadiness() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) Run(_ string, _ []LaunchModel, _ []string) error {
|
||||
return errClaudeDesktopUnsupported()
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) Restore() error {
|
||||
if err := claudeDesktopSupported(); err != nil {
|
||||
return err
|
||||
}
|
||||
targets, err := claudeDesktopTargetPaths()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, path := range targets.normalConfigs {
|
||||
if err := writeClaudeDesktopDeploymentMode(path, "1p"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, target := range targets.thirdPartyProfiles {
|
||||
if err := writeClaudeDesktopDeploymentMode(target.desktopConfig, "1p"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restoreClaudeDesktopMeta(target.meta); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restoreClaudeDesktopOllamaProfile(target.profile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return claudeDesktopLaunchOrRestart("Restart Claude Desktop to use the usual Claude profile?")
|
||||
}
|
||||
|
||||
func errClaudeDesktopUnsupported() error {
|
||||
return errors.New(claudeDesktopUnsupported)
|
||||
}
|
||||
|
||||
func claudeDesktopSupported() error {
|
||||
switch claudeDesktopGOOS {
|
||||
case "darwin", "windows":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("Claude Desktop launch is only supported on macOS and Windows")
|
||||
}
|
||||
}
|
||||
|
||||
func claudeDesktopInstalled() bool {
|
||||
if claudeDesktopAppPath() != "" {
|
||||
return true
|
||||
}
|
||||
if claudeDesktopGOOS == "windows" && claudeDesktopIsRunning() {
|
||||
return true
|
||||
}
|
||||
for _, dir := range claudeDesktopProfileDirCandidates(false) {
|
||||
if _, err := claudeDesktopStat(dir); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func claudeDesktopAppPath() string {
|
||||
if claudeDesktopGOOS != "darwin" && claudeDesktopGOOS != "windows" {
|
||||
return ""
|
||||
}
|
||||
for _, path := range claudeDesktopAppCandidates() {
|
||||
if _, err := claudeDesktopStat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func claudeDesktopAppCandidates() []string {
|
||||
switch claudeDesktopGOOS {
|
||||
case "darwin":
|
||||
return claudeDesktopDarwinAppCandidates()
|
||||
case "windows":
|
||||
return claudeDesktopWindowsAppCandidates()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func claudeDesktopDarwinAppCandidates() []string {
|
||||
candidates := []string{"/Applications/Claude.app"}
|
||||
if home, err := claudeDesktopUserHome(); err == nil {
|
||||
candidates = append(candidates, filepath.Join(home, "Applications", "Claude.app"))
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func claudeDesktopWindowsAppCandidates() []string {
|
||||
local, err := claudeDesktopLocalAppData()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
filepath.Join(local, "Programs", "Claude", "Claude.exe"),
|
||||
filepath.Join(local, "Programs", "Claude Desktop", "Claude.exe"),
|
||||
filepath.Join(local, "Claude", "Claude.exe"),
|
||||
filepath.Join(local, "Claude Nest", "Claude.exe"),
|
||||
filepath.Join(local, "Claude Desktop", "Claude.exe"),
|
||||
filepath.Join(local, "AnthropicClaude", "Claude.exe"),
|
||||
}
|
||||
for _, pattern := range []string{
|
||||
filepath.Join(local, "AnthropicClaude", "app-*", "Claude.exe"),
|
||||
filepath.Join(local, "Programs", "Claude", "app-*", "Claude.exe"),
|
||||
filepath.Join(local, "Programs", "Claude Desktop", "app-*", "Claude.exe"),
|
||||
} {
|
||||
matches, _ := claudeDesktopGlob(pattern)
|
||||
candidates = append(candidates, matches...)
|
||||
}
|
||||
return claudeDesktopDedupePaths(candidates)
|
||||
}
|
||||
|
||||
func claudeDesktopDedupePaths(paths []string) []string {
|
||||
out := make([]string, 0, len(paths))
|
||||
seen := make(map[string]bool, len(paths))
|
||||
for _, path := range paths {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(path)
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
out = append(out, path)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type claudeDesktopPaths struct {
|
||||
normalConfig string
|
||||
desktopConfig string
|
||||
meta string
|
||||
profile string
|
||||
}
|
||||
|
||||
type claudeDesktopThirdPartyPaths struct {
|
||||
desktopConfig string
|
||||
meta string
|
||||
profile string
|
||||
}
|
||||
|
||||
type claudeDesktopTargets struct {
|
||||
normalConfigs []string
|
||||
thirdPartyProfiles []claudeDesktopThirdPartyPaths
|
||||
}
|
||||
|
||||
func claudeDesktopConfigPaths() (claudeDesktopPaths, error) {
|
||||
switch claudeDesktopGOOS {
|
||||
case "darwin":
|
||||
return claudeDesktopDarwinConfigPaths()
|
||||
case "windows":
|
||||
return claudeDesktopWindowsConfigPaths()
|
||||
default:
|
||||
return claudeDesktopPaths{}, claudeDesktopSupported()
|
||||
}
|
||||
}
|
||||
|
||||
func claudeDesktopDarwinConfigPaths() (claudeDesktopPaths, error) {
|
||||
normalRoots, thirdPartyRoots, err := claudeDesktopDarwinProfileRoots()
|
||||
if err != nil {
|
||||
return claudeDesktopPaths{}, err
|
||||
}
|
||||
normalBase := normalRoots[0]
|
||||
thirdPartyBase := thirdPartyRoots[0]
|
||||
return claudeDesktopPaths{
|
||||
normalConfig: filepath.Join(normalBase, "claude_desktop_config.json"),
|
||||
desktopConfig: filepath.Join(thirdPartyBase, "claude_desktop_config.json"),
|
||||
meta: filepath.Join(thirdPartyBase, "configLibrary", "_meta.json"),
|
||||
profile: filepath.Join(thirdPartyBase, "configLibrary", claudeDesktopProfileID+".json"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func claudeDesktopWindowsConfigPaths() (claudeDesktopPaths, error) {
|
||||
normalBase, err := claudeDesktopProfileDir(true)
|
||||
if err != nil {
|
||||
return claudeDesktopPaths{}, err
|
||||
}
|
||||
thirdPartyBase, err := claudeDesktopProfileDir(false)
|
||||
if err != nil {
|
||||
return claudeDesktopPaths{}, err
|
||||
}
|
||||
return claudeDesktopPaths{
|
||||
normalConfig: filepath.Join(normalBase, "claude_desktop_config.json"),
|
||||
desktopConfig: filepath.Join(thirdPartyBase, "claude_desktop_config.json"),
|
||||
meta: filepath.Join(thirdPartyBase, "configLibrary", "_meta.json"),
|
||||
profile: filepath.Join(thirdPartyBase, "configLibrary", claudeDesktopProfileID+".json"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func claudeDesktopProfileDir(normal bool) (string, error) {
|
||||
candidates := claudeDesktopProfileDirCandidates(normal)
|
||||
if len(candidates) == 0 {
|
||||
return "", fmt.Errorf("Claude Desktop profile directory could not be resolved")
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, err := claudeDesktopStat(candidate); err == nil {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return candidates[0], nil
|
||||
}
|
||||
|
||||
func claudeDesktopProfileDirCandidates(normal bool) []string {
|
||||
if claudeDesktopGOOS != "windows" {
|
||||
return nil
|
||||
}
|
||||
normalRoots, thirdPartyRoots, err := claudeDesktopWindowsProfileRoots()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if normal {
|
||||
return normalRoots
|
||||
}
|
||||
return thirdPartyRoots
|
||||
}
|
||||
|
||||
func claudeDesktopDarwinProfileRoots() ([]string, []string, error) {
|
||||
home, err := claudeDesktopUserHome()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
base := filepath.Join(home, "Library", "Application Support")
|
||||
return []string{filepath.Join(base, "Claude")}, []string{filepath.Join(base, "Claude-3p")}, nil
|
||||
}
|
||||
|
||||
func claudeDesktopWindowsProfileRoots() ([]string, []string, error) {
|
||||
local, err := claudeDesktopLocalAppData()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
normalRoots := []string{
|
||||
filepath.Join(local, "Claude"),
|
||||
filepath.Join(local, "Claude Nest"),
|
||||
}
|
||||
thirdPartyRoots := []string{
|
||||
filepath.Join(local, "Claude-3p"),
|
||||
filepath.Join(local, "Claude Nest-3p"),
|
||||
}
|
||||
return normalRoots, thirdPartyRoots, nil
|
||||
}
|
||||
|
||||
func claudeDesktopTargetPaths() (claudeDesktopTargets, error) {
|
||||
var (
|
||||
normalRoots []string
|
||||
thirdPartyRoots []string
|
||||
err error
|
||||
)
|
||||
|
||||
switch claudeDesktopGOOS {
|
||||
case "darwin":
|
||||
normalRoots, thirdPartyRoots, err = claudeDesktopDarwinProfileRoots()
|
||||
case "windows":
|
||||
normalRoots, thirdPartyRoots, err = claudeDesktopWindowsProfileRoots()
|
||||
default:
|
||||
err = claudeDesktopSupported()
|
||||
}
|
||||
if err != nil {
|
||||
return claudeDesktopTargets{}, err
|
||||
}
|
||||
|
||||
return newClaudeDesktopTargets(normalRoots, thirdPartyRoots), nil
|
||||
}
|
||||
|
||||
func newClaudeDesktopTargets(normalRoots, thirdPartyRoots []string) claudeDesktopTargets {
|
||||
targets := claudeDesktopTargets{}
|
||||
for _, root := range claudeDesktopDedupePaths(normalRoots) {
|
||||
targets.normalConfigs = append(targets.normalConfigs, filepath.Join(root, "claude_desktop_config.json"))
|
||||
}
|
||||
for _, root := range claudeDesktopDedupePaths(thirdPartyRoots) {
|
||||
targets.thirdPartyProfiles = append(targets.thirdPartyProfiles, claudeDesktopThirdPartyPaths{
|
||||
desktopConfig: filepath.Join(root, "claude_desktop_config.json"),
|
||||
meta: filepath.Join(root, "configLibrary", "_meta.json"),
|
||||
profile: filepath.Join(root, "configLibrary", claudeDesktopProfileID+".json"),
|
||||
})
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
func claudeDesktopTargetProfilePaths(targets claudeDesktopTargets) []string {
|
||||
paths := make([]string, 0, len(targets.thirdPartyProfiles))
|
||||
for _, target := range targets.thirdPartyProfiles {
|
||||
paths = append(paths, target.profile)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func claudeDesktopLocalAppData() (string, error) {
|
||||
if local := strings.TrimSpace(os.Getenv("LOCALAPPDATA")); local != "" {
|
||||
return local, nil
|
||||
}
|
||||
if home := strings.TrimSpace(os.Getenv("USERPROFILE")); home != "" {
|
||||
return filepath.Join(home, "AppData", "Local"), nil
|
||||
}
|
||||
home, err := claudeDesktopUserHome()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, "AppData", "Local"), nil
|
||||
}
|
||||
|
||||
type claudeDesktopAPIKeySource int
|
||||
|
||||
const (
|
||||
claudeDesktopAPIKeySourceNone claudeDesktopAPIKeySource = iota
|
||||
claudeDesktopAPIKeySourceEnv
|
||||
claudeDesktopAPIKeySourceProfile
|
||||
)
|
||||
|
||||
func claudeDesktopValidatedAPIKey(ctx context.Context, profilePaths []string) (string, error) {
|
||||
key, source, err := claudeDesktopAPIKey(profilePaths)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := claudeDesktopValidateAPIKey(ctx, key); err == nil {
|
||||
return key, nil
|
||||
} else if source != claudeDesktopAPIKeySourceProfile || !canPromptClaudeDesktopAPIKey() {
|
||||
return "", err
|
||||
}
|
||||
return promptValidClaudeDesktopAPIKey(ctx)
|
||||
}
|
||||
|
||||
func claudeDesktopAPIKey(profilePaths []string) (string, claudeDesktopAPIKeySource, error) {
|
||||
if key := strings.TrimSpace(os.Getenv("OLLAMA_API_KEY")); key != "" {
|
||||
return key, claudeDesktopAPIKeySourceEnv, nil
|
||||
}
|
||||
for _, profilePath := range profilePaths {
|
||||
if key := readClaudeDesktopGatewayAPIKey(profilePath); key != "" {
|
||||
return key, claudeDesktopAPIKeySourceProfile, nil
|
||||
}
|
||||
}
|
||||
key, err := promptClaudeDesktopAPIKeyValue()
|
||||
return key, claudeDesktopAPIKeySourceNone, err
|
||||
}
|
||||
|
||||
func canPromptClaudeDesktopAPIKey() bool {
|
||||
return isInteractiveSession() && !currentLaunchConfirmPolicy.requireYesMessage
|
||||
}
|
||||
|
||||
func promptValidClaudeDesktopAPIKey(ctx context.Context) (string, error) {
|
||||
key, err := promptClaudeDesktopAPIKeyValue()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := claudeDesktopValidateAPIKey(ctx, key); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func promptClaudeDesktopAPIKeyValue() (string, error) {
|
||||
if !canPromptClaudeDesktopAPIKey() {
|
||||
return "", missingClaudeDesktopAPIKeyError()
|
||||
}
|
||||
key, err := claudeDesktopPromptAPIKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return "", missingClaudeDesktopAPIKeyError()
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func missingClaudeDesktopAPIKeyError() error {
|
||||
return fmt.Errorf("OLLAMA_API_KEY is required for Claude Desktop. Create an API key at %s, then re-run with OLLAMA_API_KEY set", claudeDesktopAPIKeyURL)
|
||||
}
|
||||
|
||||
func promptClaudeDesktopAPIKey() (string, error) {
|
||||
fmt.Fprint(os.Stderr, claudeDesktopAPIKeyPrompt())
|
||||
key, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Fprintln(os.Stderr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(key), nil
|
||||
}
|
||||
|
||||
func claudeDesktopAPIKeyPrompt() string {
|
||||
return fmt.Sprintf("Create an Ollama API key at %s\nEnter Ollama API key (input hidden): ", claudeDesktopAPIKeyURL)
|
||||
}
|
||||
|
||||
func readClaudeDesktopGatewayAPIKey(path string) string {
|
||||
cfg, err := readClaudeDesktopJSON(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
key, _ := cfg["inferenceGatewayApiKey"].(string)
|
||||
return strings.TrimSpace(key)
|
||||
}
|
||||
|
||||
func validateClaudeDesktopAPIKey(ctx context.Context, key string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if claudeDesktopAPIKeyHasInvalidHeaderChars(key) {
|
||||
return claudeDesktopAPIKeyVerificationError()
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, claudeDesktopGatewayBaseURL+"/v1/models", nil)
|
||||
if err != nil {
|
||||
return claudeDesktopAPIKeyVerificationError()
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := claudeDesktopHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return claudeDesktopAPIKeyVerificationError()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4<<10))
|
||||
|
||||
switch {
|
||||
case resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden:
|
||||
return fmt.Errorf("Ollama API key was rejected; create a valid key at %s", claudeDesktopAPIKeyURL)
|
||||
case resp.StatusCode >= 200 && resp.StatusCode < 300:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("could not verify Ollama API key; ollama.com returned status %d, try again later", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func claudeDesktopAPIKeyHasInvalidHeaderChars(key string) bool {
|
||||
return strings.ContainsFunc(key, func(r rune) bool {
|
||||
return r < ' ' || r == 0x7f
|
||||
})
|
||||
}
|
||||
|
||||
func claudeDesktopAPIKeyVerificationError() error {
|
||||
return fmt.Errorf("could not verify Ollama API key; copy a key from %s and try again", claudeDesktopAPIKeyURL)
|
||||
}
|
||||
|
||||
func writeClaudeDesktopDeploymentMode(path, mode string) error {
|
||||
cfg, err := readClaudeDesktopJSONAllowMissing(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse Claude Desktop config: %w", err)
|
||||
}
|
||||
cfg["deploymentMode"] = mode
|
||||
return writeClaudeDesktopJSON(path, cfg)
|
||||
}
|
||||
|
||||
func writeClaudeDesktopMeta(path, id, name string) error {
|
||||
meta, err := readClaudeDesktopJSONAllowMissing(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse Claude Desktop config metadata: %w", err)
|
||||
}
|
||||
|
||||
meta["appliedId"] = id
|
||||
entries := make([]any, 0)
|
||||
for _, entry := range claudeDesktopAnySlice(meta["entries"]) {
|
||||
entryMap, _ := entry.(map[string]any)
|
||||
if entryMap == nil {
|
||||
entries = append(entries, entry)
|
||||
continue
|
||||
}
|
||||
if entryID, _ := entryMap["id"].(string); entryID == id {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, entryMap)
|
||||
}
|
||||
entries = append(entries, map[string]any{
|
||||
"id": id,
|
||||
"name": name,
|
||||
})
|
||||
meta["entries"] = entries
|
||||
return writeClaudeDesktopJSON(path, meta)
|
||||
}
|
||||
|
||||
func writeClaudeDesktopGatewayProfile(path string, apiKey string, forceChooser bool) error {
|
||||
cfg, err := readClaudeDesktopJSONAllowMissing(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse Claude Desktop Ollama profile: %w", err)
|
||||
}
|
||||
cfg["inferenceProvider"] = "gateway"
|
||||
cfg["inferenceGatewayBaseUrl"] = claudeDesktopGatewayBaseURL
|
||||
cfg["inferenceGatewayApiKey"] = apiKey
|
||||
cfg["inferenceGatewayAuthScheme"] = "bearer"
|
||||
delete(cfg, "inferenceModels")
|
||||
cfg["disableDeploymentModeChooser"] = forceChooser
|
||||
return writeClaudeDesktopJSON(path, cfg)
|
||||
}
|
||||
|
||||
func restoreClaudeDesktopMeta(path string) error {
|
||||
meta, err := readClaudeDesktopJSONAllowMissing(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse Claude Desktop config metadata: %w", err)
|
||||
}
|
||||
if len(meta) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
changed := false
|
||||
if appliedID, _ := meta["appliedId"].(string); appliedID == claudeDesktopProfileID {
|
||||
delete(meta, "appliedId")
|
||||
changed = true
|
||||
}
|
||||
|
||||
entries := claudeDesktopAnySlice(meta["entries"])
|
||||
if entries != nil {
|
||||
filtered := make([]any, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
entryMap, _ := entry.(map[string]any)
|
||||
if entryID, _ := entryMap["id"].(string); entryID == claudeDesktopProfileID {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, entry)
|
||||
}
|
||||
meta["entries"] = filtered
|
||||
}
|
||||
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
return writeClaudeDesktopJSON(path, meta)
|
||||
}
|
||||
|
||||
func restoreClaudeDesktopOllamaProfile(path string) error {
|
||||
cfg, err := readClaudeDesktopJSONAllowMissing(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse Claude Desktop Ollama profile: %w", err)
|
||||
}
|
||||
if len(cfg) == 0 {
|
||||
return nil
|
||||
}
|
||||
cfg["disableDeploymentModeChooser"] = false
|
||||
delete(cfg, "inferenceProvider")
|
||||
delete(cfg, "inferenceGatewayBaseUrl")
|
||||
delete(cfg, "inferenceGatewayAuthScheme")
|
||||
delete(cfg, "inferenceModels")
|
||||
return writeClaudeDesktopJSON(path, cfg)
|
||||
}
|
||||
|
||||
func readClaudeDesktopAppliedID(path string) string {
|
||||
meta, err := readClaudeDesktopJSON(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
applied, _ := meta["appliedId"].(string)
|
||||
return applied
|
||||
}
|
||||
|
||||
func readClaudeDesktopDeploymentMode(path string) string {
|
||||
cfg, err := readClaudeDesktopJSON(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
mode, _ := cfg["deploymentMode"].(string)
|
||||
return mode
|
||||
}
|
||||
|
||||
func claudeDesktopTargetsConfigured(targets claudeDesktopTargets) bool {
|
||||
if len(targets.normalConfigs) == 0 || len(targets.thirdPartyProfiles) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, path := range targets.normalConfigs {
|
||||
if readClaudeDesktopDeploymentMode(path) != "3p" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, target := range targets.thirdPartyProfiles {
|
||||
if readClaudeDesktopDeploymentMode(target.desktopConfig) != "3p" {
|
||||
return false
|
||||
}
|
||||
if !claudeDesktopThirdPartyProfileConfigured(target) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func claudeDesktopThirdPartyProfileConfigured(target claudeDesktopThirdPartyPaths) bool {
|
||||
if readClaudeDesktopAppliedID(target.meta) != claudeDesktopProfileID {
|
||||
return false
|
||||
}
|
||||
|
||||
cfg, err := readClaudeDesktopJSON(target.profile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if s, _ := cfg["inferenceProvider"].(string); s != "gateway" {
|
||||
return false
|
||||
}
|
||||
if s, _ := cfg["inferenceGatewayBaseUrl"].(string); strings.TrimRight(s, "/") != claudeDesktopGatewayBaseURL {
|
||||
return false
|
||||
}
|
||||
if s, _ := cfg["inferenceGatewayApiKey"].(string); strings.TrimSpace(s) == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func readClaudeDesktopJSONAllowMissing(path string) (map[string]any, error) {
|
||||
cfg, err := readClaudeDesktopJSON(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func readClaudeDesktopJSON(path string) (map[string]any, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg == nil {
|
||||
cfg = map[string]any{}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func writeClaudeDesktopJSON(path string, cfg any) error {
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return fileutil.WriteWithBackup(path, data)
|
||||
}
|
||||
|
||||
func claudeDesktopAnySlice(value any) []any {
|
||||
switch v := value.(type) {
|
||||
case []any:
|
||||
return v
|
||||
case nil:
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func claudeDesktopLaunchOrRestart(prompt string) error {
|
||||
if !claudeDesktopIsRunning() {
|
||||
return claudeDesktopOpenApp()
|
||||
}
|
||||
restartAppPath := ""
|
||||
if claudeDesktopGOOS == "windows" {
|
||||
restartAppPath = claudeDesktopRunningAppPath()
|
||||
}
|
||||
|
||||
restart, err := ConfirmPrompt(prompt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !restart {
|
||||
fmt.Fprintln(os.Stderr, "\nQuit and reopen Claude Desktop when you're ready for the profile change to take effect.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := claudeDesktopQuitApp(); err != nil {
|
||||
return fmt.Errorf("quit Claude Desktop: %w", err)
|
||||
}
|
||||
if err := waitForClaudeDesktopExit(30 * time.Second); err != nil {
|
||||
return err
|
||||
}
|
||||
if restartAppPath != "" {
|
||||
return claudeDesktopOpenAppPath(restartAppPath)
|
||||
}
|
||||
return claudeDesktopOpenApp()
|
||||
}
|
||||
|
||||
func waitForClaudeDesktopExit(timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if !claudeDesktopIsRunning() {
|
||||
return nil
|
||||
}
|
||||
claudeDesktopSleep(200 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("Claude Desktop did not quit; quit it manually and re-run the command")
|
||||
}
|
||||
|
||||
func defaultClaudeDesktopIsRunning() bool {
|
||||
switch claudeDesktopGOOS {
|
||||
case "darwin":
|
||||
out, err := exec.Command("pgrep", "-f", "Claude.app/Contents/MacOS/Claude").Output()
|
||||
return err == nil && strings.TrimSpace(string(out)) != ""
|
||||
case "windows":
|
||||
out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", `(Get-Process claude -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1).Id`).Output()
|
||||
return err == nil && strings.TrimSpace(string(out)) != ""
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func defaultClaudeDesktopOpenApp() error {
|
||||
switch claudeDesktopGOOS {
|
||||
case "windows":
|
||||
if path := claudeDesktopAppPath(); path != "" {
|
||||
return claudeDesktopOpenAppPath(path)
|
||||
}
|
||||
if path := claudeDesktopRunningAppPath(); path != "" {
|
||||
return claudeDesktopOpenAppPath(path)
|
||||
}
|
||||
return fmt.Errorf("Claude Desktop executable was not found; open Claude Desktop manually once and re-run 'ollama launch claude-desktop --restore'")
|
||||
case "darwin":
|
||||
return openClaudeDesktopDarwin()
|
||||
default:
|
||||
return claudeDesktopSupported()
|
||||
}
|
||||
}
|
||||
|
||||
func defaultClaudeDesktopOpenAppPath(path string) error {
|
||||
switch claudeDesktopGOOS {
|
||||
case "windows":
|
||||
return exec.Command("powershell.exe", "-NoProfile", "-Command", "Start-Process -FilePath "+quotePowerShellString(path)).Run()
|
||||
case "darwin":
|
||||
return openClaudeDesktopDarwin()
|
||||
default:
|
||||
return claudeDesktopSupported()
|
||||
}
|
||||
}
|
||||
|
||||
func openClaudeDesktopDarwin() error {
|
||||
cmd := exec.Command("open", "-a", "Claude")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func defaultClaudeDesktopRunningAppPath() string {
|
||||
if claudeDesktopGOOS != "windows" {
|
||||
return ""
|
||||
}
|
||||
script := `(Get-Process claude -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 -and $_.Path } | Select-Object -First 1 -ExpandProperty Path)`
|
||||
out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", script).Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
func defaultClaudeDesktopQuitApp() error {
|
||||
if claudeDesktopGOOS == "windows" {
|
||||
script := `Get-Process claude -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | ForEach-Object { [void]$_.CloseMainWindow() }`
|
||||
return exec.Command("powershell.exe", "-NoProfile", "-Command", script).Run()
|
||||
}
|
||||
return exec.Command("osascript", "-e", `tell application "Claude" to quit`).Run()
|
||||
}
|
||||
|
||||
func quotePowerShellString(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
|
||||
}
|
||||
Reference in New Issue
Block a user