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.
Files changed (50) hide show
  1. package/LICENSE +201 -0
  2. package/PAT.txt +1 -0
  3. package/README.md +95 -0
  4. package/cli.js +92 -0
  5. package/error/err1.txt +12 -0
  6. package/package.json +31 -0
  7. package/setup.js +67 -0
  8. package/src/ghostcode/__init__.py +3 -0
  9. package/src/ghostcode/__main__.py +3 -0
  10. package/src/ghostcode/_version.py +17 -0
  11. package/src/ghostcode/agents/__init__.py +20 -0
  12. package/src/ghostcode/agents/code.py +104 -0
  13. package/src/ghostcode/agents/explore.py +50 -0
  14. package/src/ghostcode/ai/__init__.py +0 -0
  15. package/src/ghostcode/ai/anthropic_client.py +8 -0
  16. package/src/ghostcode/ai/base.py +33 -0
  17. package/src/ghostcode/ai/factory.py +38 -0
  18. package/src/ghostcode/ai/groq_client.py +7 -0
  19. package/src/ghostcode/ai/nvidia_client.py +7 -0
  20. package/src/ghostcode/ai/ollama_client.py +7 -0
  21. package/src/ghostcode/ai/openai_client.py +7 -0
  22. package/src/ghostcode/ai/openai_compat.py +139 -0
  23. package/src/ghostcode/ai/opencode_go_client.py +7 -0
  24. package/src/ghostcode/ai/opencode_zen_client.py +7 -0
  25. package/src/ghostcode/ai/openrouter_client.py +7 -0
  26. package/src/ghostcode/ai/registry.py +169 -0
  27. package/src/ghostcode/app.py +148 -0
  28. package/src/ghostcode/config.py +24 -0
  29. package/src/ghostcode/core/__init__.py +5 -0
  30. package/src/ghostcode/core/commands.py +55 -0
  31. package/src/ghostcode/core/file_ops.py +127 -0
  32. package/src/ghostcode/core/hooks.py +122 -0
  33. package/src/ghostcode/core/memory.py +137 -0
  34. package/src/ghostcode/core/prompt.py +73 -0
  35. package/src/ghostcode/core/reminders.py +61 -0
  36. package/src/ghostcode/core/security.py +119 -0
  37. package/src/ghostcode/core/tasks.py +125 -0
  38. package/src/ghostcode/tools/__init__.py +2 -0
  39. package/src/ghostcode/tools/defs.py +325 -0
  40. package/src/ghostcode/tools/executor.py +261 -0
  41. package/src/ghostcode/tui/__init__.py +0 -0
  42. package/src/ghostcode/tui/app.py +12 -0
  43. package/src/ghostcode/tui/dialogs.py +241 -0
  44. package/src/ghostcode/tui/screens.py +799 -0
  45. package/src/ghostcode/tui/widgets.py +32 -0
  46. package/src/ghostcode/utils/__init__.py +0 -0
  47. package/src/ghostcode/utils/config_loader.py +102 -0
  48. package/src/ghostcode/utils/logger.py +16 -0
  49. package/src/pyproject.toml +18 -0
  50. 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,8 @@
1
+ from ghostcode.ai.base import BaseAIClient
2
+ from ghostcode.ai.openai_compat import OpenAICompatClient
3
+
4
+
5
+ class AnthropicClient(OpenAICompatClient):
6
+ @property
7
+ def provider_name(self) -> str:
8
+ return "anthropic"
@@ -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,7 @@
1
+ from ghostcode.ai.openai_compat import OpenAICompatClient
2
+
3
+
4
+ class GroqClient(OpenAICompatClient):
5
+ @property
6
+ def provider_name(self) -> str:
7
+ return "groq"
@@ -0,0 +1,7 @@
1
+ from ghostcode.ai.openai_compat import OpenAICompatClient
2
+
3
+
4
+ class NvidiaClient(OpenAICompatClient):
5
+ @property
6
+ def provider_name(self) -> str:
7
+ return "nvidia"
@@ -0,0 +1,7 @@
1
+ from ghostcode.ai.openai_compat import OpenAICompatClient
2
+
3
+
4
+ class OllamaClient(OpenAICompatClient):
5
+ @property
6
+ def provider_name(self) -> str:
7
+ return "ollama"
@@ -0,0 +1,7 @@
1
+ from ghostcode.ai.openai_compat import OpenAICompatClient
2
+
3
+
4
+ class OpenAIClient(OpenAICompatClient):
5
+ @property
6
+ def provider_name(self) -> str:
7
+ return "openai"
@@ -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,7 @@
1
+ from ghostcode.ai.openai_compat import OpenAICompatClient
2
+
3
+
4
+ class OpenCodeGoClient(OpenAICompatClient):
5
+ @property
6
+ def provider_name(self) -> str:
7
+ return "opencode-go"
@@ -0,0 +1,7 @@
1
+ from ghostcode.ai.openai_compat import OpenAICompatClient
2
+
3
+
4
+ class OpenCodeZenClient(OpenAICompatClient):
5
+ @property
6
+ def provider_name(self) -> str:
7
+ return "opencode-zen"
@@ -0,0 +1,7 @@
1
+ from ghostcode.ai.openai_compat import OpenAICompatClient
2
+
3
+
4
+ class OpenRouterClient(OpenAICompatClient):
5
+ @property
6
+ def provider_name(self) -> str:
7
+ return "openrouter"
@@ -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