jcapy 1.1.8__tar.gz
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.
- jcapy-1.1.8/PKG-INFO +7 -0
- jcapy-1.1.8/pyproject.toml +19 -0
- jcapy-1.1.8/setup.cfg +4 -0
- jcapy-1.1.8/src/jcapy/__init__.py +0 -0
- jcapy-1.1.8/src/jcapy/commands/__init__.py +0 -0
- jcapy-1.1.8/src/jcapy/commands/brain.py +535 -0
- jcapy-1.1.8/src/jcapy/commands/doctor.py +229 -0
- jcapy-1.1.8/src/jcapy/commands/project.py +299 -0
- jcapy-1.1.8/src/jcapy/commands/skills.py +772 -0
- jcapy-1.1.8/src/jcapy/commands/sync.py +284 -0
- jcapy-1.1.8/src/jcapy/config.py +115 -0
- jcapy-1.1.8/src/jcapy/core.py +9 -0
- jcapy-1.1.8/src/jcapy/main.py +227 -0
- jcapy-1.1.8/src/jcapy/ui/__init__.py +0 -0
- jcapy-1.1.8/src/jcapy/ui/menu.py +60 -0
- jcapy-1.1.8/src/jcapy/ui/renderer.py +100 -0
- jcapy-1.1.8/src/jcapy/ui/theme.py +22 -0
- jcapy-1.1.8/src/jcapy/ui/tui.py +349 -0
- jcapy-1.1.8/src/jcapy/utils/__init__.py +0 -0
- jcapy-1.1.8/src/jcapy/utils/git_lib.py +42 -0
- jcapy-1.1.8/src/jcapy/utils/updates.py +107 -0
- jcapy-1.1.8/src/jcapy.egg-info/PKG-INFO +7 -0
- jcapy-1.1.8/src/jcapy.egg-info/SOURCES.txt +25 -0
- jcapy-1.1.8/src/jcapy.egg-info/dependency_links.txt +1 -0
- jcapy-1.1.8/src/jcapy.egg-info/entry_points.txt +2 -0
- jcapy-1.1.8/src/jcapy.egg-info/requires.txt +1 -0
- jcapy-1.1.8/src/jcapy.egg-info/top_level.txt +1 -0
jcapy-1.1.8/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jcapy"
|
|
7
|
+
version = "1.1.8"
|
|
8
|
+
description = "JCapy CLI - One-Army Orchestrator"
|
|
9
|
+
authors = [{name = "Irfan Ali", email = "nazrijz336@gmail.com"}]
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"rich>=13.0.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
jcapy = "jcapy.main:main"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["src"]
|
jcapy-1.1.8/setup.cfg
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
import subprocess
|
|
7
|
+
from jcapy.config import (
|
|
8
|
+
load_config, save_config, get_api_key,
|
|
9
|
+
JCAPY_HOME, DEFAULT_LIBRARY_PATH, BASE_DIR, LOGO_PATH
|
|
10
|
+
)
|
|
11
|
+
from jcapy.ui.menu import interactive_menu
|
|
12
|
+
# from jcapy.ui.tui import run as run_tui # We import this inside functions to avoid circular issues or early curses init
|
|
13
|
+
|
|
14
|
+
# ANSI Colors
|
|
15
|
+
CYAN = '\033[1;36m'
|
|
16
|
+
GREEN = '\033[1;32m'
|
|
17
|
+
YELLOW = '\033[1;33m'
|
|
18
|
+
MAGENTA = '\033[1;35m'
|
|
19
|
+
BLUE = '\033[1;34m'
|
|
20
|
+
WHITE = '\033[1;37m'
|
|
21
|
+
RED = '\033[1;31m'
|
|
22
|
+
BOLD = '\033[1m'
|
|
23
|
+
RESET = '\033[0m'
|
|
24
|
+
GREY = '\033[0;90m'
|
|
25
|
+
|
|
26
|
+
def migrate_persona_libraries():
|
|
27
|
+
"""Auto-migrates persona libraries from Cellar (volatile) to ~/.jcapy (persistent)"""
|
|
28
|
+
config = load_config()
|
|
29
|
+
if "personas" not in config: return
|
|
30
|
+
|
|
31
|
+
migrated = False
|
|
32
|
+
|
|
33
|
+
for name, persona in config['personas'].items():
|
|
34
|
+
if name == 'programmer': continue # handled by get_default_library_path
|
|
35
|
+
|
|
36
|
+
old_path = persona.get('path', '')
|
|
37
|
+
|
|
38
|
+
# Detect if the library is in the dangerous 'Cellar' or 'brew' path
|
|
39
|
+
# Or if it starts with BASE_DIR which might be the cellar
|
|
40
|
+
is_volatile = "Cellar/jcapy" in old_path or (BASE_DIR in old_path and JCAPY_HOME not in old_path)
|
|
41
|
+
|
|
42
|
+
if is_volatile:
|
|
43
|
+
new_path = os.path.join(JCAPY_HOME, f"library_{name}")
|
|
44
|
+
|
|
45
|
+
# If path changed, we need to migrate
|
|
46
|
+
if os.path.abspath(old_path) != os.path.abspath(new_path):
|
|
47
|
+
print(f"{YELLOW}📦 Migrating persona '{name}' to persistent storage...{RESET}")
|
|
48
|
+
|
|
49
|
+
if os.path.exists(old_path):
|
|
50
|
+
if not os.path.exists(new_path):
|
|
51
|
+
try:
|
|
52
|
+
shutil.copytree(old_path, new_path)
|
|
53
|
+
print(f"{GREEN}✔ Data moved to {new_path}{RESET}")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"{RED}Failed to move data: {e}{RESET}")
|
|
56
|
+
continue # Don't update config if copy failed
|
|
57
|
+
else:
|
|
58
|
+
print(f"{GREY}Target {new_path} already exists. Updating config pointer.{RESET}")
|
|
59
|
+
else:
|
|
60
|
+
print(f"{RED}⚠️ Warning: '{name}' library path was lost/missing. Pointing to new location.{RESET}")
|
|
61
|
+
if not os.path.exists(new_path):
|
|
62
|
+
os.makedirs(new_path, exist_ok=True)
|
|
63
|
+
os.makedirs(os.path.join(new_path, "skills"), exist_ok=True)
|
|
64
|
+
|
|
65
|
+
persona['path'] = new_path
|
|
66
|
+
migrated = True
|
|
67
|
+
|
|
68
|
+
if migrated:
|
|
69
|
+
save_config(config)
|
|
70
|
+
print(f"{GREEN}✔ Migration complete. All personas secured in {JCAPY_HOME}{RESET}\n")
|
|
71
|
+
|
|
72
|
+
# ==========================================
|
|
73
|
+
# PERSONA LOGIC
|
|
74
|
+
# ==========================================
|
|
75
|
+
def load_face():
|
|
76
|
+
"""Loads and displays the jcapy Face (logo.md)"""
|
|
77
|
+
if os.path.exists(LOGO_PATH):
|
|
78
|
+
print(CYAN)
|
|
79
|
+
with open(LOGO_PATH, 'r') as f:
|
|
80
|
+
print(f.read())
|
|
81
|
+
print(RESET)
|
|
82
|
+
else:
|
|
83
|
+
print(f"{CYAN}🤖 jcapy{RESET}")
|
|
84
|
+
|
|
85
|
+
def open_brain_vscode():
|
|
86
|
+
"""Opens the entire jcapy Brain (~/.jcapy) in VS Code"""
|
|
87
|
+
if not os.path.exists(JCAPY_HOME):
|
|
88
|
+
os.makedirs(JCAPY_HOME, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
print(f"📂 Opening jcapy Brain at {JCAPY_HOME}...")
|
|
91
|
+
|
|
92
|
+
# Try VS Code first
|
|
93
|
+
if shutil.which('code'):
|
|
94
|
+
subprocess.call(['code', JCAPY_HOME])
|
|
95
|
+
elif sys.platform == 'darwin':
|
|
96
|
+
subprocess.call(['open', JCAPY_HOME])
|
|
97
|
+
else:
|
|
98
|
+
print(f"{RED}VS Code ('code') not found in PATH.{RESET}")
|
|
99
|
+
|
|
100
|
+
def select_persona():
|
|
101
|
+
"""Interactive Persona Selection & Configuration"""
|
|
102
|
+
load_face()
|
|
103
|
+
|
|
104
|
+
config = load_config()
|
|
105
|
+
current = config.get("current_persona", "programmer")
|
|
106
|
+
|
|
107
|
+
print(f"{WHITE}Hi I'm jcapy, your personal assistant.{RESET}\n")
|
|
108
|
+
print(f"{GREY}Current Brain: {JCAPY_HOME}{RESET}\n")
|
|
109
|
+
|
|
110
|
+
# 1. Build Options with Status
|
|
111
|
+
if "personas" not in config: config["personas"] = {}
|
|
112
|
+
if "programmer" not in config["personas"]: config["personas"]["programmer"] = {"path": DEFAULT_LIBRARY_PATH}
|
|
113
|
+
|
|
114
|
+
persona_keys = ["programmer"] + sorted([k for k in config["personas"].keys() if k != "programmer"])
|
|
115
|
+
menu_options = []
|
|
116
|
+
|
|
117
|
+
# Avoid circular import for git checks if possible, or lazy import
|
|
118
|
+
from jcapy.utils.git_lib import get_git_status
|
|
119
|
+
|
|
120
|
+
for p in persona_keys:
|
|
121
|
+
p_data = config["personas"].get(p, {})
|
|
122
|
+
p_path = p_data.get("path", DEFAULT_LIBRARY_PATH)
|
|
123
|
+
lock_status = " 🔒" if p_data.get("locked") else ""
|
|
124
|
+
|
|
125
|
+
# Get Git Status
|
|
126
|
+
last_sync, pending = get_git_status(p_path)
|
|
127
|
+
|
|
128
|
+
display_name = p.capitalize()
|
|
129
|
+
if p == "programmer": display_name += " (One-Army Protocol)"
|
|
130
|
+
|
|
131
|
+
option_str = f"{display_name}{lock_status}"
|
|
132
|
+
if last_sync:
|
|
133
|
+
status_icon = "✅" if pending == 0 else "🛠️"
|
|
134
|
+
option_str += f"\n ↳ {GREY}Last Sync: {last_sync} | Pending: {pending} {status_icon}{RESET}"
|
|
135
|
+
|
|
136
|
+
menu_options.append(option_str)
|
|
137
|
+
|
|
138
|
+
# 2. Add Shortcuts
|
|
139
|
+
menu_options.append("Manage Personas")
|
|
140
|
+
|
|
141
|
+
# 3. Determine Default Index
|
|
142
|
+
default_idx = 0
|
|
143
|
+
if current in persona_keys:
|
|
144
|
+
default_idx = persona_keys.index(current)
|
|
145
|
+
|
|
146
|
+
# 4. Show Interactive Menu with Shortcuts Handler
|
|
147
|
+
prompt_text = f"Who is operating right now? (Current: {current.upper()})"
|
|
148
|
+
|
|
149
|
+
# Lazy import push to handle 'P' shortcut
|
|
150
|
+
from jcapy.commands.sync import push_all_personas
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
choice_idx, char_code = interactive_menu(prompt_text, menu_options, default_index=default_idx, return_char=True)
|
|
154
|
+
|
|
155
|
+
if char_code in ['c', 'C']:
|
|
156
|
+
open_brain_vscode()
|
|
157
|
+
continue # Refresh menu
|
|
158
|
+
elif char_code in ['s', 'S']:
|
|
159
|
+
from jcapy.commands.sync import sync_all_personas
|
|
160
|
+
sync_all_personas()
|
|
161
|
+
input("Press Enter to continue...")
|
|
162
|
+
continue # Refresh menu
|
|
163
|
+
elif char_code in ['p', 'P']:
|
|
164
|
+
push_all_personas()
|
|
165
|
+
input("Press Enter to continue...")
|
|
166
|
+
continue # Refresh menu
|
|
167
|
+
else:
|
|
168
|
+
break # Selection made
|
|
169
|
+
|
|
170
|
+
# 5. Map Choice to Persona
|
|
171
|
+
manage_index = len(menu_options) - 1
|
|
172
|
+
|
|
173
|
+
if choice_idx == manage_index:
|
|
174
|
+
manage_personas_menu()
|
|
175
|
+
return select_persona()
|
|
176
|
+
|
|
177
|
+
persona_key = persona_keys[choice_idx]
|
|
178
|
+
lib_path = config["personas"].get(persona_key, {}).get("path", DEFAULT_LIBRARY_PATH)
|
|
179
|
+
|
|
180
|
+
# Ensure directory exists
|
|
181
|
+
if not os.path.exists(lib_path):
|
|
182
|
+
try:
|
|
183
|
+
os.makedirs(os.path.join(lib_path, "skills"))
|
|
184
|
+
os.makedirs(os.path.join(lib_path, "scripts"))
|
|
185
|
+
print(f"{GREEN}✔ Created new memory bank for {persona_key}{RESET}")
|
|
186
|
+
except:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
# Save Config
|
|
190
|
+
config["current_persona"] = persona_key
|
|
191
|
+
|
|
192
|
+
# Update personas dict
|
|
193
|
+
if "personas" not in config: config["personas"] = {}
|
|
194
|
+
if "programmer" not in config["personas"]: config["personas"]["programmer"] = {"path": DEFAULT_LIBRARY_PATH}
|
|
195
|
+
if persona_key not in config["personas"]:
|
|
196
|
+
config["personas"][persona_key] = {"path": lib_path}
|
|
197
|
+
|
|
198
|
+
save_config(config)
|
|
199
|
+
|
|
200
|
+
print(f"{YELLOW}User identified: {persona_key.upper()}{RESET}")
|
|
201
|
+
|
|
202
|
+
# Transition to TUI directly
|
|
203
|
+
from jcapy.ui.tui import run as run_tui
|
|
204
|
+
run_tui(lib_path)
|
|
205
|
+
|
|
206
|
+
def manage_personas_menu():
|
|
207
|
+
"""Menu to manage (Rename, Lock, Delete) personas"""
|
|
208
|
+
while True:
|
|
209
|
+
options = [
|
|
210
|
+
"➕ Create New Persona",
|
|
211
|
+
"✏️ Rename Persona",
|
|
212
|
+
"🔒 Lock/Unlock Persona",
|
|
213
|
+
"🗑️ Delete Persona",
|
|
214
|
+
"⬅️ Back"
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
choice_idx = interactive_menu("🛠️ Persona Management", options)
|
|
218
|
+
|
|
219
|
+
if choice_idx == 0:
|
|
220
|
+
add_persona()
|
|
221
|
+
elif choice_idx == 1:
|
|
222
|
+
rename_persona()
|
|
223
|
+
elif choice_idx == 2:
|
|
224
|
+
lock_persona()
|
|
225
|
+
elif choice_idx == 3:
|
|
226
|
+
delete_persona()
|
|
227
|
+
elif choice_idx == 4:
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
def add_persona():
|
|
231
|
+
print(f"\n{MAGENTA}➕ Create New Persona{RESET}")
|
|
232
|
+
name = input(f"{CYAN}? Persona Name (give it any name): {RESET}").strip().lower()
|
|
233
|
+
if not name: return
|
|
234
|
+
|
|
235
|
+
# Normalize
|
|
236
|
+
name = re.sub(r'[^a-z0-9_]', '', name)
|
|
237
|
+
|
|
238
|
+
config = load_config()
|
|
239
|
+
if "personas" not in config: config["personas"] = {}
|
|
240
|
+
|
|
241
|
+
if name in config["personas"] or name == "programmer":
|
|
242
|
+
print(f"{RED}Error: Persona '{name}' already exists.{RESET}")
|
|
243
|
+
time.sleep(1)
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
lib_dir = f"library_{name}"
|
|
247
|
+
lib_path = os.path.join(JCAPY_HOME, lib_dir)
|
|
248
|
+
|
|
249
|
+
if not os.path.exists(lib_path):
|
|
250
|
+
os.makedirs(lib_path, exist_ok=True)
|
|
251
|
+
# Create subfolders
|
|
252
|
+
os.makedirs(os.path.join(lib_path, "skills"), exist_ok=True)
|
|
253
|
+
os.makedirs(os.path.join(lib_path, "scripts"), exist_ok=True)
|
|
254
|
+
|
|
255
|
+
config["personas"][name] = {
|
|
256
|
+
"path": lib_path,
|
|
257
|
+
"created_at": str(time.time()), # Using timestamp
|
|
258
|
+
"locked": False
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
save_config(config)
|
|
262
|
+
print(f"{GREEN}✔ Persona '{name}' added at {lib_path}!{RESET}")
|
|
263
|
+
time.sleep(1)
|
|
264
|
+
|
|
265
|
+
def rename_persona():
|
|
266
|
+
config = load_config()
|
|
267
|
+
dynamic_personas = sorted([k for k in config.get("personas", {}).keys() if k != "programmer"])
|
|
268
|
+
|
|
269
|
+
if not dynamic_personas:
|
|
270
|
+
print(f"{YELLOW}No dynamic personas to rename.{RESET}")
|
|
271
|
+
time.sleep(1)
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
print(f"\n{YELLOW}✏️ Rename Persona{RESET}")
|
|
275
|
+
|
|
276
|
+
options = []
|
|
277
|
+
for p in dynamic_personas:
|
|
278
|
+
options.append(p)
|
|
279
|
+
options.append("Cancel")
|
|
280
|
+
|
|
281
|
+
choice_idx = interactive_menu("Select Persona to rename", options)
|
|
282
|
+
|
|
283
|
+
if choice_idx >= len(dynamic_personas):
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
old_name = dynamic_personas[choice_idx]
|
|
287
|
+
|
|
288
|
+
if config["personas"][old_name].get("locked"):
|
|
289
|
+
print(f"{RED}Error: Persona '{old_name}' is locked. Unlock it first.{RESET}")
|
|
290
|
+
time.sleep(1)
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
new_name = input(f"{CYAN}? New Name for '{old_name}': {RESET}").strip().lower()
|
|
294
|
+
if not new_name: return
|
|
295
|
+
new_name = re.sub(r'[^a-z0-9_]', '', new_name)
|
|
296
|
+
|
|
297
|
+
if new_name in config["personas"] or new_name == "programmer":
|
|
298
|
+
print(f"{RED}Error: Name '{new_name}' already exists.{RESET}")
|
|
299
|
+
time.sleep(1)
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
# Rename directory if it matches library_{old_name}
|
|
303
|
+
old_path = config["personas"][old_name]["path"]
|
|
304
|
+
new_lib_dir = f"library_{new_name}"
|
|
305
|
+
new_path = os.path.join(JCAPY_HOME, new_lib_dir)
|
|
306
|
+
|
|
307
|
+
# Standard check logic from monolithic
|
|
308
|
+
standard_old_base = os.path.join(BASE_DIR, f"library_{old_name}")
|
|
309
|
+
standard_old_home = os.path.join(JCAPY_HOME, f"library_{old_name}")
|
|
310
|
+
|
|
311
|
+
if old_path == standard_old_base or old_path == standard_old_home:
|
|
312
|
+
if os.path.exists(old_path):
|
|
313
|
+
if os.path.exists(new_path):
|
|
314
|
+
print(f"{RED}Error: Target path {new_path} already exists.{RESET}")
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
os.rename(old_path, new_path)
|
|
318
|
+
|
|
319
|
+
config["personas"][new_name] = config["personas"].pop(old_name)
|
|
320
|
+
config["personas"][new_name]["path"] = new_path
|
|
321
|
+
else:
|
|
322
|
+
# Custom path? Just rename key, don't move folder
|
|
323
|
+
config["personas"][new_name] = config["personas"].pop(old_name)
|
|
324
|
+
|
|
325
|
+
save_config(config)
|
|
326
|
+
print(f"{GREEN}✔ Persona renamed to '{new_name}'!{RESET}")
|
|
327
|
+
time.sleep(1)
|
|
328
|
+
|
|
329
|
+
def lock_persona():
|
|
330
|
+
config = load_config()
|
|
331
|
+
dynamic_personas = sorted([k for k in config.get("personas", {}).keys() if k != "programmer"])
|
|
332
|
+
|
|
333
|
+
if not dynamic_personas:
|
|
334
|
+
print(f"{YELLOW}No dynamic personas to lock/unlock.{RESET}")
|
|
335
|
+
time.sleep(1)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
print(f"\n{CYAN}🔒 Lock/Unlock Persona{RESET}")
|
|
339
|
+
|
|
340
|
+
options = []
|
|
341
|
+
for p in dynamic_personas:
|
|
342
|
+
status = "🔒 Locked" if config["personas"][p].get("locked") else "🔓 Unlocked"
|
|
343
|
+
options.append(f"{p} ({status})")
|
|
344
|
+
options.append("Cancel")
|
|
345
|
+
|
|
346
|
+
choice_idx = interactive_menu("Select Persona to Lock/Unlock", options)
|
|
347
|
+
|
|
348
|
+
if choice_idx >= len(dynamic_personas):
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
p_name = dynamic_personas[choice_idx]
|
|
352
|
+
is_locked = config["personas"][p_name].get("locked", False)
|
|
353
|
+
config["personas"][p_name]["locked"] = not is_locked
|
|
354
|
+
|
|
355
|
+
save_config(config)
|
|
356
|
+
new_status = "Locked" if not is_locked else "Unlocked"
|
|
357
|
+
print(f"{GREEN}✔ Persona '{p_name}' is now {new_status}.{RESET}")
|
|
358
|
+
time.sleep(1)
|
|
359
|
+
|
|
360
|
+
def delete_persona():
|
|
361
|
+
config = load_config()
|
|
362
|
+
dynamic_personas = sorted([k for k in config.get("personas", {}).keys() if k != "programmer"])
|
|
363
|
+
|
|
364
|
+
if not dynamic_personas:
|
|
365
|
+
print(f"{YELLOW}No dynamic personas to delete.{RESET}")
|
|
366
|
+
time.sleep(1)
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
print(f"\n{RED}🗑️ Delete Persona{RESET}")
|
|
370
|
+
|
|
371
|
+
options = []
|
|
372
|
+
for p in dynamic_personas:
|
|
373
|
+
lock_status = " 🔒" if config["personas"][p].get("locked") else ""
|
|
374
|
+
options.append(f"{p}{lock_status}")
|
|
375
|
+
options.append("Cancel")
|
|
376
|
+
|
|
377
|
+
choice_idx = interactive_menu("Select Persona to DELETE", options)
|
|
378
|
+
|
|
379
|
+
if choice_idx >= len(dynamic_personas):
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
p_name = dynamic_personas[choice_idx]
|
|
383
|
+
|
|
384
|
+
if config["personas"][p_name].get("locked"):
|
|
385
|
+
print(f"{RED}Error: Persona '{p_name}' is locked. Unlock it first.{RESET}")
|
|
386
|
+
time.sleep(1)
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
print(f"\n{RED}⚠️ WARNING: You are about to delete '{p_name}'.{RESET}")
|
|
390
|
+
print(f"{RED}This will PERMANENTLY delete all persona data and its library files!{RESET}")
|
|
391
|
+
confirm = input(f"{YELLOW}? Type '{p_name}' to confirm deletion: {RESET}").strip()
|
|
392
|
+
|
|
393
|
+
if confirm == p_name:
|
|
394
|
+
lib_path = config["personas"][p_name]["path"]
|
|
395
|
+
|
|
396
|
+
# Delete library directory
|
|
397
|
+
if os.path.exists(lib_path):
|
|
398
|
+
shutil.rmtree(lib_path)
|
|
399
|
+
print(f"{YELLOW}✔ Deleted memory bank at {lib_path}{RESET}")
|
|
400
|
+
|
|
401
|
+
# Remove from config
|
|
402
|
+
del config["personas"][p_name]
|
|
403
|
+
if config.get("current_persona") == p_name:
|
|
404
|
+
config["current_persona"] = "programmer"
|
|
405
|
+
|
|
406
|
+
save_config(config)
|
|
407
|
+
print(f"{GREEN}✔ Persona '{p_name}' deleted successfully.{RESET}")
|
|
408
|
+
else:
|
|
409
|
+
print(f"{YELLOW}Deletion aborted. Confirmation name didn't match.{RESET}")
|
|
410
|
+
|
|
411
|
+
time.sleep(1)
|
|
412
|
+
|
|
413
|
+
# ==========================================
|
|
414
|
+
# BRAINSTORMING (AI) LOGIC
|
|
415
|
+
# ==========================================
|
|
416
|
+
|
|
417
|
+
def check_api_keys():
|
|
418
|
+
"""Ensures API keys are present (Env or Config)."""
|
|
419
|
+
# We only need to load config if we plan to save to it
|
|
420
|
+
config = load_config()
|
|
421
|
+
if 'env' not in config: config['env'] = {}
|
|
422
|
+
|
|
423
|
+
providers = ['gemini', 'openai', 'deepseek'] # Lowercase for get_api_key
|
|
424
|
+
changed = False
|
|
425
|
+
|
|
426
|
+
print(f"\n{MAGENTA}🔑 AI Configuration {RESET}")
|
|
427
|
+
for p in providers:
|
|
428
|
+
key_val = get_api_key(p)
|
|
429
|
+
status = f"{GREEN}Found (Env/Config){RESET}" if key_val else f"{RED}Missing{RESET}"
|
|
430
|
+
|
|
431
|
+
# Display Status
|
|
432
|
+
print(f" • {p.capitalize()}: {status}")
|
|
433
|
+
|
|
434
|
+
if not key_val:
|
|
435
|
+
val = input(f" {CYAN}Enter {p.capitalize()} Key (or leave blank to skip): {RESET}").strip()
|
|
436
|
+
if val:
|
|
437
|
+
# Save to config (Securely)
|
|
438
|
+
env_key = f"{p.upper()}_API_KEY"
|
|
439
|
+
config['env'][env_key] = val
|
|
440
|
+
changed = True
|
|
441
|
+
|
|
442
|
+
if changed:
|
|
443
|
+
save_config(config)
|
|
444
|
+
print(f"{GREEN}Keys saved securely (0o600).{RESET}")
|
|
445
|
+
else:
|
|
446
|
+
print(f"{GREY}Configuration checks out.{RESET}")
|
|
447
|
+
|
|
448
|
+
def get_brainstorm_prompt(context_type="skill"):
|
|
449
|
+
"""Returns the 2026 Fortress Standard Prompt."""
|
|
450
|
+
return """
|
|
451
|
+
You are the **jcapy Architect Bot**. Your goal is to **refactor** the provided code into a 2026 "Fortress" Standard skill.
|
|
452
|
+
|
|
453
|
+
### Core Constraints:
|
|
454
|
+
1. **Idempotency**: Ensure `mkdir`, `cd`, and `cat` operations don't fail if they've already run.
|
|
455
|
+
2. **Pre-flight Checks**: Add logic to verify dependencies (e.g., `command -v kubectl`) before executing.
|
|
456
|
+
3. **Observability**: Use emojis and clear `echo` statements for every major step.
|
|
457
|
+
4. **Modern Patterns**: Use `[[ ]]` for shell tests and ensure all variables are quoted to handle spaces.
|
|
458
|
+
5. **Harvest-Ready**: Maintain the `<!-- jcapy:EXEC -->` anchor and the Markdown structure.
|
|
459
|
+
|
|
460
|
+
### Output Format:
|
|
461
|
+
Return **only** the improved Markdown content. Do not include conversational filler.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
def brainstorm_skill(target_path, provider="local"):
|
|
465
|
+
"""Executes the brainstorming session."""
|
|
466
|
+
|
|
467
|
+
# 1. Read Target Context
|
|
468
|
+
if not os.path.exists(target_path):
|
|
469
|
+
print(f"{RED}Error: Target {target_path} not found.{RESET}")
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
with open(target_path, 'r') as f:
|
|
473
|
+
content = f.read()
|
|
474
|
+
|
|
475
|
+
# 2. Prepare Prompt
|
|
476
|
+
system_prompt = get_brainstorm_prompt()
|
|
477
|
+
full_request = f"{system_prompt}\n\n--- TARGET CONTEXT ---\n{content}"
|
|
478
|
+
|
|
479
|
+
# 3. Execute
|
|
480
|
+
if provider == "local":
|
|
481
|
+
out_file = target_path + ".prompt.txt"
|
|
482
|
+
with open(out_file, 'w') as f:
|
|
483
|
+
f.write(full_request)
|
|
484
|
+
print(f"\n{GREEN}📝 Brainstorm Context generated:{RESET} {out_file}")
|
|
485
|
+
print(f"{GREY}Copy this content into Gemini/ChatGPT/DeepSeek to get your refactor.{RESET}")
|
|
486
|
+
|
|
487
|
+
# Auto-open
|
|
488
|
+
if shutil.which('code'):
|
|
489
|
+
subprocess.call(['code', out_file])
|
|
490
|
+
elif sys.platform == 'darwin':
|
|
491
|
+
subprocess.call(['open', out_file])
|
|
492
|
+
|
|
493
|
+
elif provider in ["gemini", "openai", "deepseek"]:
|
|
494
|
+
print(f"{YELLOW}📡 Connecting to {provider.upper()} (Simulation)...{RESET}")
|
|
495
|
+
# Placeholder for actual API call
|
|
496
|
+
print(f"{RED}API Integration not yet fully hydrated. Switching to Local Dump.{RESET}")
|
|
497
|
+
brainstorm_skill(target_path, "local")
|
|
498
|
+
|
|
499
|
+
def run_brainstorm_wizard():
|
|
500
|
+
"""Interactive Wizard for jcapy Brainstorm."""
|
|
501
|
+
print(f"\n{MAGENTA}🧠 jcapy Brainstorm (AI Refactor){RESET}")
|
|
502
|
+
|
|
503
|
+
# 1. Select Target (Simple: Current Directory's files)
|
|
504
|
+
files = [f for f in os.listdir('.') if f.endswith('.md') or f.endswith('.py') or f.endswith('.sh')]
|
|
505
|
+
if not files:
|
|
506
|
+
print(f"{RED}No suitable files found in current directory.{RESET}")
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
print(f"\n{CYAN}Select Target to Refactor:{RESET}")
|
|
510
|
+
menu_options = files + ["Cancel"]
|
|
511
|
+
choice = interactive_menu("Make a choice:", menu_options)
|
|
512
|
+
|
|
513
|
+
if choice == len(menu_options) - 1: return
|
|
514
|
+
target_file = files[choice]
|
|
515
|
+
|
|
516
|
+
# 2. Select Provider
|
|
517
|
+
print(f"\n{CYAN}Select Intelligence Engine:{RESET}")
|
|
518
|
+
providers = ["Local (Generate Prompt File)", "Gemini", "OpenAI", "DeepSeek"]
|
|
519
|
+
p_choice = interactive_menu("Choose Provider:", providers)
|
|
520
|
+
|
|
521
|
+
p_map = {0: "local", 1: "gemini", 2: "openai", 3: "deepseek"}
|
|
522
|
+
selected_provider = p_map[p_choice]
|
|
523
|
+
|
|
524
|
+
# 3. Check Keys if Cloud
|
|
525
|
+
if selected_provider != "local":
|
|
526
|
+
# Check if key is available via Env or Config
|
|
527
|
+
if not get_api_key(selected_provider):
|
|
528
|
+
print(f"{YELLOW}⚠️ Key for {selected_provider} not found in Env or Config.{RESET}")
|
|
529
|
+
check_api_keys() # Trigger setup
|
|
530
|
+
if not get_api_key(selected_provider):
|
|
531
|
+
print(f"{RED}Aborted. No key provided.{RESET}")
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
# 4. Execute
|
|
535
|
+
brainstorm_skill(target_file, selected_provider)
|