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 ADDED
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: jcapy
3
+ Version: 1.1.8
4
+ Summary: JCapy CLI - One-Army Orchestrator
5
+ Author-email: Irfan Ali <nazrijz336@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: rich>=13.0.0
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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)