ghostcode-canary 0.1.25-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/PAT.txt +1 -0
- package/README.md +95 -0
- package/cli.js +92 -0
- package/error/err1.txt +12 -0
- package/package.json +31 -0
- package/setup.js +67 -0
- package/src/ghostcode/__init__.py +3 -0
- package/src/ghostcode/__main__.py +3 -0
- package/src/ghostcode/_version.py +17 -0
- package/src/ghostcode/agents/__init__.py +20 -0
- package/src/ghostcode/agents/code.py +104 -0
- package/src/ghostcode/agents/explore.py +50 -0
- package/src/ghostcode/ai/__init__.py +0 -0
- package/src/ghostcode/ai/anthropic_client.py +8 -0
- package/src/ghostcode/ai/base.py +33 -0
- package/src/ghostcode/ai/factory.py +38 -0
- package/src/ghostcode/ai/groq_client.py +7 -0
- package/src/ghostcode/ai/nvidia_client.py +7 -0
- package/src/ghostcode/ai/ollama_client.py +7 -0
- package/src/ghostcode/ai/openai_client.py +7 -0
- package/src/ghostcode/ai/openai_compat.py +139 -0
- package/src/ghostcode/ai/opencode_go_client.py +7 -0
- package/src/ghostcode/ai/opencode_zen_client.py +7 -0
- package/src/ghostcode/ai/openrouter_client.py +7 -0
- package/src/ghostcode/ai/registry.py +169 -0
- package/src/ghostcode/app.py +148 -0
- package/src/ghostcode/config.py +24 -0
- package/src/ghostcode/core/__init__.py +5 -0
- package/src/ghostcode/core/commands.py +55 -0
- package/src/ghostcode/core/file_ops.py +127 -0
- package/src/ghostcode/core/hooks.py +122 -0
- package/src/ghostcode/core/memory.py +137 -0
- package/src/ghostcode/core/prompt.py +73 -0
- package/src/ghostcode/core/reminders.py +61 -0
- package/src/ghostcode/core/security.py +119 -0
- package/src/ghostcode/core/tasks.py +125 -0
- package/src/ghostcode/tools/__init__.py +2 -0
- package/src/ghostcode/tools/defs.py +325 -0
- package/src/ghostcode/tools/executor.py +261 -0
- package/src/ghostcode/tui/__init__.py +0 -0
- package/src/ghostcode/tui/app.py +12 -0
- package/src/ghostcode/tui/dialogs.py +241 -0
- package/src/ghostcode/tui/screens.py +799 -0
- package/src/ghostcode/tui/widgets.py +32 -0
- package/src/ghostcode/utils/__init__.py +0 -0
- package/src/ghostcode/utils/config_loader.py +102 -0
- package/src/ghostcode/utils/logger.py +16 -0
- package/src/pyproject.toml +18 -0
- package/token.txt +1 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
SYSTEM_PROMPT = """You are a file search specialist and software architect for GhostCode. You excel at thoroughly navigating and exploring codebases to find information and design implementation plans.
|
|
2
|
+
|
|
3
|
+
=== CRITICAL: READ-ONLY MODE ===
|
|
4
|
+
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
|
|
5
|
+
- Creating new files (no Write, touch, or file creation of any kind)
|
|
6
|
+
- Modifying existing files (no Edit operations)
|
|
7
|
+
- Deleting files (no rm or deletion)
|
|
8
|
+
- Moving or copying files (no mv or cp)
|
|
9
|
+
- Creating temporary files anywhere
|
|
10
|
+
- Using redirect operators (>, >>, |) or heredocs to write to files
|
|
11
|
+
- Running ANY commands that change system state
|
|
12
|
+
|
|
13
|
+
Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools — attempting to edit files will fail.
|
|
14
|
+
|
|
15
|
+
Your strengths:
|
|
16
|
+
- Rapidly finding files using glob patterns
|
|
17
|
+
- Searching code with powerful regex patterns
|
|
18
|
+
- Reading and analyzing file contents
|
|
19
|
+
|
|
20
|
+
# Process
|
|
21
|
+
|
|
22
|
+
1. Understand requirements. Read any context or files provided.
|
|
23
|
+
|
|
24
|
+
2. Explore thoroughly — make efficient parallel tool calls:
|
|
25
|
+
- Use grep_search and list_files for targeted searches
|
|
26
|
+
- Use read_file when you know the specific file path
|
|
27
|
+
- Use run_command ONLY for read-only operations: Get-ChildItem, git status, git log, git diff, Get-Content
|
|
28
|
+
- NEVER use run_command for: New-Item, Remove-Item, Copy-Item, Move-Item, git add, git commit, npm install, pip install
|
|
29
|
+
- Before asking a clarifying question, spend time investigating first
|
|
30
|
+
|
|
31
|
+
3. Design the solution:
|
|
32
|
+
- Consider trade-offs and architectural decisions
|
|
33
|
+
- Follow existing patterns where appropriate
|
|
34
|
+
- Identify dependencies and sequencing
|
|
35
|
+
- Be efficient — make parallel tool calls where possible
|
|
36
|
+
|
|
37
|
+
4. Detail the plan:
|
|
38
|
+
- Provide step-by-step implementation strategy
|
|
39
|
+
- Identify 3-5 critical files most important for implementing
|
|
40
|
+
- Note build commands, test setup, and verification steps
|
|
41
|
+
|
|
42
|
+
# Communication
|
|
43
|
+
|
|
44
|
+
Lead with the outcome. First sentence should answer "what did you find" or "what's the plan." Supporting detail comes after. Write so the reader can pick up cold: complete sentences, no unexplained jargon. When referencing code, include file_path:line_number.
|
|
45
|
+
|
|
46
|
+
Match the response to the ask: a quick search gets a direct answer, not sections. A full plan gets the structure it needs.
|
|
47
|
+
|
|
48
|
+
NOTE: You are meant to be a fast agent that returns output as quickly as possible. Make efficient use of tools and spawn multiple parallel tool calls for searching and reading files.
|
|
49
|
+
|
|
50
|
+
Complete the user's search request efficiently and report your findings clearly."""
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import AsyncGenerator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseAIClient(ABC):
|
|
6
|
+
@property
|
|
7
|
+
def supports_tools(self) -> bool:
|
|
8
|
+
return False
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def generate(
|
|
12
|
+
self, messages: list[dict], stream: bool = True
|
|
13
|
+
) -> AsyncGenerator[str, None]:
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
async def chat(
|
|
17
|
+
self,
|
|
18
|
+
messages: list[dict],
|
|
19
|
+
tools: list | None = None,
|
|
20
|
+
permission_callback=None,
|
|
21
|
+
) -> AsyncGenerator[str, None]:
|
|
22
|
+
yield "[dim]tool-calling not supported by this provider[/dim]"
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def provider_name(self) -> str:
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def model_name(self) -> str:
|
|
33
|
+
...
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from ghostcode.ai.anthropic_client import AnthropicClient
|
|
2
|
+
from ghostcode.ai.base import BaseAIClient
|
|
3
|
+
from ghostcode.ai.groq_client import GroqClient
|
|
4
|
+
from ghostcode.ai.nvidia_client import NvidiaClient
|
|
5
|
+
from ghostcode.ai.ollama_client import OllamaClient
|
|
6
|
+
from ghostcode.ai.opencode_go_client import OpenCodeGoClient
|
|
7
|
+
from ghostcode.ai.opencode_zen_client import OpenCodeZenClient
|
|
8
|
+
from ghostcode.ai.openai_client import OpenAIClient
|
|
9
|
+
from ghostcode.ai.openrouter_client import OpenRouterClient
|
|
10
|
+
from ghostcode.ai.registry import PROVIDERS
|
|
11
|
+
from ghostcode.utils.config_loader import get_api_key
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_client(provider: str | None = None, model: str | None = None) -> BaseAIClient:
|
|
15
|
+
provider = (provider or "nvidia").lower()
|
|
16
|
+
info = PROVIDERS.get(provider)
|
|
17
|
+
if not info:
|
|
18
|
+
raise ValueError(f"Unknown provider: {provider}")
|
|
19
|
+
|
|
20
|
+
model = model or info["default_model"]
|
|
21
|
+
api_key = get_api_key(provider)
|
|
22
|
+
base_url = info["base_url"]
|
|
23
|
+
|
|
24
|
+
cls_map = {
|
|
25
|
+
"nvidia": NvidiaClient,
|
|
26
|
+
"openai": OpenAIClient,
|
|
27
|
+
"groq": GroqClient,
|
|
28
|
+
"anthropic": AnthropicClient,
|
|
29
|
+
"ollama": OllamaClient,
|
|
30
|
+
"openrouter": OpenRouterClient,
|
|
31
|
+
"opencode-zen": OpenCodeZenClient,
|
|
32
|
+
"opencode-go": OpenCodeGoClient,
|
|
33
|
+
}
|
|
34
|
+
client_cls = cls_map.get(provider)
|
|
35
|
+
if not client_cls:
|
|
36
|
+
raise ValueError(f"Provider not implemented: {provider}")
|
|
37
|
+
|
|
38
|
+
return client_cls(api_key=api_key, model=model, base_url=base_url)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import AsyncGenerator
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from ghostcode.ai.base import BaseAIClient
|
|
7
|
+
from ghostcode.tools.executor import execute_tool, needs_permission
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _first_choice(data: dict) -> dict:
|
|
11
|
+
choices = data.get("choices")
|
|
12
|
+
if choices and len(choices) > 0:
|
|
13
|
+
return choices[0]
|
|
14
|
+
return {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OpenAICompatClient(BaseAIClient):
|
|
18
|
+
def __init__(self, api_key: str, model: str, base_url: str):
|
|
19
|
+
self._api_key = api_key
|
|
20
|
+
self._model = model
|
|
21
|
+
self._base_url = base_url.rstrip("/")
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def model_name(self) -> str:
|
|
25
|
+
return self._model
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def supports_tools(self) -> bool:
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
def _headers(self) -> dict:
|
|
32
|
+
h = {"Content-Type": "application/json"}
|
|
33
|
+
if self._api_key:
|
|
34
|
+
h["Authorization"] = f"Bearer {self._api_key}"
|
|
35
|
+
return h
|
|
36
|
+
|
|
37
|
+
async def _post(self, url: str, payload: dict) -> dict:
|
|
38
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(120.0)) as client:
|
|
39
|
+
resp = await client.post(url, json=payload, headers=self._headers())
|
|
40
|
+
resp.raise_for_status()
|
|
41
|
+
return resp.json()
|
|
42
|
+
|
|
43
|
+
async def generate(self, messages: list[dict], stream: bool = True) -> AsyncGenerator[str, None]:
|
|
44
|
+
url = f"{self._base_url}/chat/completions"
|
|
45
|
+
payload = {
|
|
46
|
+
"model": self._model,
|
|
47
|
+
"messages": messages,
|
|
48
|
+
"stream": stream,
|
|
49
|
+
"max_tokens": 4096,
|
|
50
|
+
"temperature": 0.3,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(120.0)) as client:
|
|
54
|
+
if stream:
|
|
55
|
+
async with client.stream("POST", url, json=payload, headers=self._headers()) as resp:
|
|
56
|
+
resp.raise_for_status()
|
|
57
|
+
async for line in resp.aiter_lines():
|
|
58
|
+
if not line or line.startswith(":"):
|
|
59
|
+
continue
|
|
60
|
+
if line.startswith("data: "):
|
|
61
|
+
data = line[6:]
|
|
62
|
+
if data.strip() == "[DONE]":
|
|
63
|
+
break
|
|
64
|
+
try:
|
|
65
|
+
chunk = json.loads(data)
|
|
66
|
+
choice = _first_choice(chunk)
|
|
67
|
+
delta = choice.get("delta", {})
|
|
68
|
+
content = delta.get("content", "")
|
|
69
|
+
if content:
|
|
70
|
+
yield content
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
continue
|
|
73
|
+
else:
|
|
74
|
+
resp = await client.post(url, json=payload, headers=self._headers())
|
|
75
|
+
resp.raise_for_status()
|
|
76
|
+
data = resp.json()
|
|
77
|
+
choice = _first_choice(data)
|
|
78
|
+
content = choice.get("message", {}).get("content", "")
|
|
79
|
+
if content:
|
|
80
|
+
yield content
|
|
81
|
+
|
|
82
|
+
async def chat(
|
|
83
|
+
self,
|
|
84
|
+
messages: list[dict],
|
|
85
|
+
tools: list | None = None,
|
|
86
|
+
permission_callback=None,
|
|
87
|
+
) -> AsyncGenerator[str, None]:
|
|
88
|
+
msgs = list(messages)
|
|
89
|
+
url = f"{self._base_url}/chat/completions"
|
|
90
|
+
base_payload = {
|
|
91
|
+
"model": self._model,
|
|
92
|
+
"max_tokens": 4096,
|
|
93
|
+
"temperature": 0.3,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
while tools:
|
|
97
|
+
payload = {**base_payload, "messages": msgs, "tools": tools, "stream": False}
|
|
98
|
+
yield "[dim]...thinking[/dim]\n"
|
|
99
|
+
|
|
100
|
+
data = await self._post(url, payload)
|
|
101
|
+
choice = _first_choice(data)
|
|
102
|
+
msg = choice.get("message", {})
|
|
103
|
+
|
|
104
|
+
tool_calls = msg.get("tool_calls")
|
|
105
|
+
if not tool_calls:
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
msgs.append({"role": "assistant", "content": msg.get("content") or "", "tool_calls": tool_calls})
|
|
109
|
+
|
|
110
|
+
for tc in tool_calls:
|
|
111
|
+
func = tc.get("function", {})
|
|
112
|
+
tool_name = func.get("name", "")
|
|
113
|
+
try:
|
|
114
|
+
args = json.loads(func.get("arguments", "{}"))
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
args = {}
|
|
117
|
+
|
|
118
|
+
yield f"[dim]→ {tool_name}[/dim]\n"
|
|
119
|
+
|
|
120
|
+
if permission_callback and needs_permission(tool_name, args):
|
|
121
|
+
allowed = await permission_callback(tool_name, args)
|
|
122
|
+
if not allowed:
|
|
123
|
+
msgs.append({
|
|
124
|
+
"role": "tool",
|
|
125
|
+
"tool_call_id": tc["id"],
|
|
126
|
+
"content": json.dumps({"error": "Permission denied by user"}),
|
|
127
|
+
})
|
|
128
|
+
yield f"[dim] denied[/dim]\n"
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
result = await execute_tool(tool_name, args)
|
|
132
|
+
msgs.append({
|
|
133
|
+
"role": "tool",
|
|
134
|
+
"tool_call_id": tc["id"],
|
|
135
|
+
"content": result,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
async for chunk in self.generate(msgs, stream=True):
|
|
139
|
+
yield chunk
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
PROVIDERS = {
|
|
2
|
+
"opencode-zen": {
|
|
3
|
+
"name": "OpenCode Zen",
|
|
4
|
+
"description": "Curated pay-as-you-go coding models",
|
|
5
|
+
"default_model": "deepseek-v4-flash",
|
|
6
|
+
"base_url": "https://opencode.ai/zen/v1",
|
|
7
|
+
"env_var": "OPENCODE_API_KEY",
|
|
8
|
+
"needs_key": True,
|
|
9
|
+
"models": [
|
|
10
|
+
"deepseek-v4-pro",
|
|
11
|
+
"deepseek-v4-flash",
|
|
12
|
+
"deepseek-v4-flash-free",
|
|
13
|
+
"kimi-k2.7-code",
|
|
14
|
+
"kimi-k2.6",
|
|
15
|
+
"minimax-m3",
|
|
16
|
+
"minimax-m2.7",
|
|
17
|
+
"qwen3.7-max",
|
|
18
|
+
"qwen3.7-plus",
|
|
19
|
+
"qwen3.6-plus",
|
|
20
|
+
"grok-4.5",
|
|
21
|
+
"glm-5.2",
|
|
22
|
+
"glm-5.1",
|
|
23
|
+
"big-pickle",
|
|
24
|
+
"mimo-v2.5-free",
|
|
25
|
+
"gpt-5.6-sol",
|
|
26
|
+
"gpt-5.6-terra",
|
|
27
|
+
"gpt-5.6-luna",
|
|
28
|
+
"gpt-5.5",
|
|
29
|
+
"gpt-5.4",
|
|
30
|
+
"gpt-5.4-mini",
|
|
31
|
+
"gpt-5.4-nano",
|
|
32
|
+
"gpt-5.3-codex",
|
|
33
|
+
"gpt-5.3-codex-spark",
|
|
34
|
+
"gpt-5.2-codex",
|
|
35
|
+
"gpt-5.1-codex",
|
|
36
|
+
"claude-sonnet-4-6",
|
|
37
|
+
"claude-sonnet-4-5",
|
|
38
|
+
"claude-haiku-4-5",
|
|
39
|
+
"gemini-3.6-flash",
|
|
40
|
+
"gemini-3.5-flash",
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
"opencode-go": {
|
|
44
|
+
"name": "OpenCode Go",
|
|
45
|
+
"description": "$10/mo subscription — open coding models",
|
|
46
|
+
"default_model": "deepseek-v4-flash",
|
|
47
|
+
"base_url": "https://opencode.ai/zen/go/v1",
|
|
48
|
+
"env_var": "OPENCODE_API_KEY",
|
|
49
|
+
"needs_key": True,
|
|
50
|
+
"models": [
|
|
51
|
+
"deepseek-v4-pro",
|
|
52
|
+
"deepseek-v4-flash",
|
|
53
|
+
"kimi-k2.7-code",
|
|
54
|
+
"kimi-k2.6",
|
|
55
|
+
"minimax-m3",
|
|
56
|
+
"minimax-m2.7",
|
|
57
|
+
"qwen3.7-max",
|
|
58
|
+
"qwen3.7-plus",
|
|
59
|
+
"qwen3.6-plus",
|
|
60
|
+
"grok-4.5",
|
|
61
|
+
"glm-5.2",
|
|
62
|
+
"glm-5.1",
|
|
63
|
+
"mimo-v2.5",
|
|
64
|
+
"mimo-v2.5-pro",
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
"nvidia": {
|
|
68
|
+
"name": "NVIDIA",
|
|
69
|
+
"description": "NVIDIA AI Foundation / NIM",
|
|
70
|
+
"default_model": "meta/llama-3.1-70b-instruct",
|
|
71
|
+
"base_url": "https://integrate.api.nvidia.com/v1",
|
|
72
|
+
"env_var": "NVIDIA_API_KEY",
|
|
73
|
+
"needs_key": True,
|
|
74
|
+
"models": [
|
|
75
|
+
"meta/llama-3.1-70b-instruct",
|
|
76
|
+
"meta/llama-3.1-8b-instruct",
|
|
77
|
+
"mistralai/mistral-7b-instruct-v0.3",
|
|
78
|
+
"mistralai/mistral-large",
|
|
79
|
+
"google/gemma-2-27b-it",
|
|
80
|
+
"google/gemma-2-9b-it",
|
|
81
|
+
"nvidia/llama-3.1-nemotron-70b-instruct",
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
"openai": {
|
|
85
|
+
"name": "OpenAI",
|
|
86
|
+
"description": "OpenAI GPT-4 / GPT-4o",
|
|
87
|
+
"default_model": "gpt-4o",
|
|
88
|
+
"base_url": "https://api.openai.com/v1",
|
|
89
|
+
"env_var": "OPENAI_API_KEY",
|
|
90
|
+
"needs_key": True,
|
|
91
|
+
"models": [
|
|
92
|
+
"gpt-4o",
|
|
93
|
+
"gpt-4o-mini",
|
|
94
|
+
"gpt-4-turbo",
|
|
95
|
+
"gpt-4",
|
|
96
|
+
"gpt-3.5-turbo",
|
|
97
|
+
"o1-mini",
|
|
98
|
+
"o1-preview",
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
"groq": {
|
|
102
|
+
"name": "Groq",
|
|
103
|
+
"description": "Groq LPU inference — fast LLMs",
|
|
104
|
+
"default_model": "llama-3.3-70b-versatile",
|
|
105
|
+
"base_url": "https://api.groq.com/openai/v1",
|
|
106
|
+
"env_var": "GROQ_API_KEY",
|
|
107
|
+
"needs_key": True,
|
|
108
|
+
"models": [
|
|
109
|
+
"llama-3.3-70b-versatile",
|
|
110
|
+
"llama-3.3-70b-specdec",
|
|
111
|
+
"llama-3.1-8b-instant",
|
|
112
|
+
"mixtral-8x7b-32768",
|
|
113
|
+
"gemma2-9b-it",
|
|
114
|
+
"llama-guard-3-8b",
|
|
115
|
+
"openai/gpt-oss-120b",
|
|
116
|
+
"openai/gpt-oss-20b",
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
"anthropic": {
|
|
120
|
+
"name": "Anthropic",
|
|
121
|
+
"description": "Anthropic Claude 3.5 Sonnet",
|
|
122
|
+
"default_model": "claude-3-5-sonnet-20241022",
|
|
123
|
+
"base_url": "https://api.anthropic.com/v1",
|
|
124
|
+
"env_var": "ANTHROPIC_API_KEY",
|
|
125
|
+
"needs_key": True,
|
|
126
|
+
"models": [
|
|
127
|
+
"claude-3-5-sonnet-20241022",
|
|
128
|
+
"claude-3-5-haiku-20241022",
|
|
129
|
+
"claude-3-opus-20240229",
|
|
130
|
+
],
|
|
131
|
+
"status": "coming_soon",
|
|
132
|
+
},
|
|
133
|
+
"ollama": {
|
|
134
|
+
"name": "Ollama",
|
|
135
|
+
"description": "Local models via Ollama",
|
|
136
|
+
"default_model": "llama3.1",
|
|
137
|
+
"base_url": "http://localhost:11434",
|
|
138
|
+
"env_var": "",
|
|
139
|
+
"needs_key": False,
|
|
140
|
+
"models": ["llama3.1", "llama3", "mistral", "codellama", "gemma2"],
|
|
141
|
+
"status": "coming_soon",
|
|
142
|
+
},
|
|
143
|
+
"openrouter": {
|
|
144
|
+
"name": "OpenRouter",
|
|
145
|
+
"description": "Unified API — many models",
|
|
146
|
+
"default_model": "meta-llama/llama-3.1-70b-instruct",
|
|
147
|
+
"base_url": "https://openrouter.ai/api/v1",
|
|
148
|
+
"env_var": "OPENROUTER_API_KEY",
|
|
149
|
+
"needs_key": True,
|
|
150
|
+
"models": [
|
|
151
|
+
"meta-llama/llama-3.1-70b-instruct",
|
|
152
|
+
"meta-llama/llama-3.1-8b-instruct",
|
|
153
|
+
"mistralai/mixtral-8x22b-instruct",
|
|
154
|
+
"qwen/qwen-2.5-72b-instruct",
|
|
155
|
+
],
|
|
156
|
+
"status": "coming_soon",
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def provider_status(pid: str, has_key: bool) -> str:
|
|
162
|
+
info = PROVIDERS[pid]
|
|
163
|
+
if info.get("status") == "coming_soon":
|
|
164
|
+
return "coming soon"
|
|
165
|
+
if info["needs_key"] and not has_key:
|
|
166
|
+
return "needs key"
|
|
167
|
+
if info["needs_key"] and has_key:
|
|
168
|
+
return "connected"
|
|
169
|
+
return "ready"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from ghostcode._version import get_version
|
|
8
|
+
from ghostcode.tui.app import GhostCodeApp
|
|
9
|
+
from ghostcode.utils.config_loader import find_and_load_env, migrate_env_keys_to_config
|
|
10
|
+
from ghostcode.utils.logger import setup_logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cmd_update():
|
|
14
|
+
import httpx
|
|
15
|
+
import re
|
|
16
|
+
current = get_version()
|
|
17
|
+
print(f"GhostCode v{current}")
|
|
18
|
+
print("Checking for updates...")
|
|
19
|
+
try:
|
|
20
|
+
r = httpx.get("https://registry.npmjs.org/ghostcode-canary/latest", timeout=5)
|
|
21
|
+
if r.status_code != 200:
|
|
22
|
+
print("Failed to check for updates.")
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
latest = r.json().get("version", "")
|
|
25
|
+
if not latest:
|
|
26
|
+
print("No version info from registry.")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
from ghostcode.tui.screens import ChatScreen
|
|
30
|
+
if not ChatScreen._is_newer(latest, current):
|
|
31
|
+
print(f"Already up to date (v{current}).")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
print(f"Update available: v{current} -> v{latest}")
|
|
35
|
+
print()
|
|
36
|
+
print(f" Downloading v{latest}...")
|
|
37
|
+
|
|
38
|
+
proc = subprocess.Popen(
|
|
39
|
+
["npm", "i", "-g", "ghostcode-canary"],
|
|
40
|
+
stdout=subprocess.PIPE,
|
|
41
|
+
stderr=subprocess.STDOUT,
|
|
42
|
+
text=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
step_re = re.compile(r"\[(\d+)/(\d+)\]")
|
|
46
|
+
last_msg = ""
|
|
47
|
+
while True:
|
|
48
|
+
line = proc.stdout.readline()
|
|
49
|
+
if not line and proc.poll() is not None:
|
|
50
|
+
break
|
|
51
|
+
if not line:
|
|
52
|
+
continue
|
|
53
|
+
line = line.strip()
|
|
54
|
+
if not line:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
m = step_re.search(line)
|
|
58
|
+
if m:
|
|
59
|
+
done, total = int(m.group(1)), int(m.group(2))
|
|
60
|
+
pct = int(done / total * 100) if total > 0 else 0
|
|
61
|
+
bar_len = 20
|
|
62
|
+
filled = int(bar_len * done / total) if total > 0 else 0
|
|
63
|
+
bar = "█" * filled + "░" * (bar_len - filled)
|
|
64
|
+
msg = f" [{bar}] {pct}% ({done}/{total} steps)"
|
|
65
|
+
if msg != last_msg:
|
|
66
|
+
print(f"\r{msg}", end="", flush=True)
|
|
67
|
+
last_msg = msg
|
|
68
|
+
elif "added" in line.lower() or "updated" in line.lower():
|
|
69
|
+
print(f"\r {line}")
|
|
70
|
+
elif "err" in line.lower() or "error" in line.lower():
|
|
71
|
+
print(f"\r {line}")
|
|
72
|
+
|
|
73
|
+
proc.wait()
|
|
74
|
+
print()
|
|
75
|
+
|
|
76
|
+
if proc.returncode == 0:
|
|
77
|
+
print(f" Updated to v{latest}. Run 'ghostcode' to start.")
|
|
78
|
+
else:
|
|
79
|
+
print(" Update failed.")
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
except FileNotFoundError:
|
|
82
|
+
print("npm not found. Install Node.js first.")
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Update failed: {e}")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cmd_uninstall(clean: bool):
|
|
90
|
+
print("GhostCode uninstall...")
|
|
91
|
+
config_dir = os.path.expanduser("~/.ghostcode")
|
|
92
|
+
|
|
93
|
+
if clean:
|
|
94
|
+
if os.path.exists(config_dir):
|
|
95
|
+
print(f"Removing config: {config_dir}")
|
|
96
|
+
shutil.rmtree(config_dir, ignore_errors=True)
|
|
97
|
+
print("Config removed.")
|
|
98
|
+
else:
|
|
99
|
+
print("No config found, skipping.")
|
|
100
|
+
else:
|
|
101
|
+
print("Config kept. Use --clean to also remove config and API keys.")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
print("Uninstalling npm package...")
|
|
105
|
+
result = subprocess.run(
|
|
106
|
+
["npm", "uninstall", "-g", "ghostcode-canary"],
|
|
107
|
+
capture_output=True, text=True,
|
|
108
|
+
)
|
|
109
|
+
if result.returncode == 0:
|
|
110
|
+
print("GhostCode uninstalled.")
|
|
111
|
+
else:
|
|
112
|
+
print("Failed to uninstall npm package:")
|
|
113
|
+
print(result.stderr)
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
except FileNotFoundError:
|
|
116
|
+
print("npm not found. Install Node.js first.")
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
parser = argparse.ArgumentParser()
|
|
122
|
+
parser.add_argument("--version", action="store_true", help="show version")
|
|
123
|
+
args, rest = parser.parse_known_args()
|
|
124
|
+
|
|
125
|
+
if args.version:
|
|
126
|
+
print(f"GhostCode v{get_version()}")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
if rest and rest[0] == "update":
|
|
130
|
+
cmd_update()
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
if rest and rest[0] == "uninstall":
|
|
134
|
+
clean = "--clean" in rest
|
|
135
|
+
cmd_uninstall(clean)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
find_and_load_env()
|
|
139
|
+
migrate_env_keys_to_config()
|
|
140
|
+
|
|
141
|
+
setup_logger("ghostcode", os.getenv("GHOSTCODE_LOG_LEVEL", "WARNING"))
|
|
142
|
+
|
|
143
|
+
app = GhostCodeApp()
|
|
144
|
+
app.run()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from ghostcode.ai.registry import PROVIDERS
|
|
2
|
+
from ghostcode.utils.config_loader import get_api_key, get_default_provider
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Settings:
|
|
6
|
+
@property
|
|
7
|
+
def providers(self):
|
|
8
|
+
return PROVIDERS
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def default_provider(self) -> str:
|
|
12
|
+
return get_default_provider()
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def api_key(self) -> str:
|
|
16
|
+
return get_api_key(self.default_provider)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def log_level(self) -> str:
|
|
20
|
+
import os
|
|
21
|
+
return os.getenv("GHOSTCODE_LOG_LEVEL", "WARNING")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
settings = Settings()
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from ghostcode.core.memory import save_memory, search_memory, delete_memory, list_memories, memory_summary, count_memories
|
|
2
|
+
from ghostcode.core.security import is_action_denied, needs_confirmation, scan_for_secrets, redact_secrets, add_deny_rule, remove_deny_rule, list_deny_rules
|
|
3
|
+
from ghostcode.core.hooks import hook_registry
|
|
4
|
+
from ghostcode.core.tasks import task_manager, TaskStatus
|
|
5
|
+
from ghostcode.core.commands import Command, register, dispatch, get_command, list_commands
|