conduct-cli 0.4.96__tar.gz → 0.4.98__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.
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/PKG-INFO +1 -1
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/pyproject.toml +1 -1
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/hook_template.py +11 -2
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/main.py +60 -131
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/memory.py +22 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/README.md +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/setup.cfg +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/setup.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/hook_precompact_template.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/hook_stop_template.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.4.96 → conduct_cli-0.4.98}/tests/test_switch.py +0 -0
|
@@ -588,12 +588,21 @@ def main():
|
|
|
588
588
|
if data.get("hook_event_name") == "Stop" or data.get("stop_hook_active"):
|
|
589
589
|
session_id = data.get("session_id", "")
|
|
590
590
|
transcript_path = data.get("transcript_path")
|
|
591
|
-
# Detect repo from CWD git remote
|
|
592
591
|
repo = _detect_repo()
|
|
593
|
-
from conduct_cli.memory import post_session_to_api
|
|
592
|
+
from conduct_cli.memory import post_session_to_api, mark_flushed
|
|
594
593
|
post_session_to_api(session_id, transcript_path, repo)
|
|
594
|
+
mark_flushed()
|
|
595
595
|
sys.exit(0)
|
|
596
596
|
|
|
597
|
+
# Periodic flush — fire at most once every 8 hours mid-session
|
|
598
|
+
from conduct_cli.memory import should_periodic_flush, mark_flushed, post_session_to_api
|
|
599
|
+
if should_periodic_flush():
|
|
600
|
+
session_id = data.get("session_id", "")
|
|
601
|
+
transcript_path = data.get("transcript_path")
|
|
602
|
+
repo = _detect_repo()
|
|
603
|
+
post_session_to_api(session_id, transcript_path, repo)
|
|
604
|
+
mark_flushed()
|
|
605
|
+
|
|
597
606
|
# Policy version check (cached 60s) — auto-syncs if server version differs
|
|
598
607
|
_maybe_sync_policy()
|
|
599
608
|
|
|
@@ -156,7 +156,7 @@ def _stream_run(server: str, workflow_id: str, run_id: str, workspace_id: str, t
|
|
|
156
156
|
print(f"{RED} ✗ {prefix}{err}{RESET}")
|
|
157
157
|
elif kind == "brain_tool_call":
|
|
158
158
|
summary = payload.get("summary", payload.get("tool", ""))
|
|
159
|
-
print(f"
|
|
159
|
+
print(f" · {summary}{RESET}")
|
|
160
160
|
elif kind == "run_completed":
|
|
161
161
|
print(f"{BOLD}{GREEN} ✓ done{RESET}")
|
|
162
162
|
elif kind == "run_failed":
|
|
@@ -198,92 +198,36 @@ def _poll_run(server: str, workflow_id: str, run_id: str, hdrs: dict) -> bool:
|
|
|
198
198
|
|
|
199
199
|
# ── Commands ──────────────────────────────────────────────────────────────────
|
|
200
200
|
|
|
201
|
-
def
|
|
202
|
-
"""Write conduct-mcp into
|
|
203
|
-
|
|
204
|
-
try:
|
|
205
|
-
existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
206
|
-
servers = existing.setdefault("mcpServers", {})
|
|
207
|
-
if "conduct" in servers:
|
|
208
|
-
return True # already registered
|
|
209
|
-
servers["conduct"] = {"command": "conduct-mcp", "args": []}
|
|
210
|
-
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
211
|
-
settings_path.write_text(json.dumps(existing, indent=2))
|
|
212
|
-
return True
|
|
213
|
-
except Exception:
|
|
214
|
-
return False
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def _write_codex_mcp_config() -> bool:
|
|
218
|
-
"""Write conduct-mcp into ~/.codex/config.toml. Returns True if written."""
|
|
219
|
-
codex_dir = Path.home() / ".codex"
|
|
220
|
-
if not codex_dir.exists():
|
|
221
|
-
return False
|
|
222
|
-
config_path = codex_dir / "config.toml"
|
|
223
|
-
try:
|
|
224
|
-
content = config_path.read_text() if config_path.exists() else ""
|
|
225
|
-
if "conduct-mcp" in content:
|
|
226
|
-
return True
|
|
227
|
-
mcp_block = '\n[mcp_servers.conduct]\ncommand = "conduct-mcp"\nargs = []\n'
|
|
228
|
-
config_path.write_text(content + mcp_block)
|
|
229
|
-
return True
|
|
230
|
-
except Exception:
|
|
231
|
-
return False
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def _write_cursor_mcp_config() -> bool:
|
|
235
|
-
"""Write conduct-mcp into ~/.cursor/mcp.json. Returns True if written."""
|
|
236
|
-
cursor_dir = Path.home() / ".cursor"
|
|
237
|
-
if not cursor_dir.exists():
|
|
201
|
+
def _write_mcp_config(path: Path, *, keys: tuple = ("mcpServers",), create: bool = False) -> bool:
|
|
202
|
+
"""Write conduct-mcp entry into a JSON config file. Returns True if written or already present."""
|
|
203
|
+
if not create and not path.parent.exists():
|
|
238
204
|
return False
|
|
239
|
-
config_path = cursor_dir / "mcp.json"
|
|
240
205
|
try:
|
|
241
|
-
existing = json.loads(
|
|
242
|
-
|
|
206
|
+
existing = json.loads(path.read_text()) if path.exists() else {}
|
|
207
|
+
node = existing
|
|
208
|
+
for k in keys[:-1]:
|
|
209
|
+
node = node.setdefault(k, {})
|
|
210
|
+
servers = node.setdefault(keys[-1], {})
|
|
243
211
|
if "conduct" in servers:
|
|
244
212
|
return True
|
|
245
213
|
servers["conduct"] = {"command": "conduct-mcp", "args": []}
|
|
246
|
-
|
|
214
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
215
|
+
path.write_text(json.dumps(existing, indent=2))
|
|
247
216
|
return True
|
|
248
217
|
except Exception:
|
|
249
218
|
return False
|
|
250
219
|
|
|
251
220
|
|
|
252
|
-
def
|
|
253
|
-
"""Write conduct-mcp into ~/.
|
|
254
|
-
config_path = Path.home() / ".
|
|
221
|
+
def _write_codex_mcp_config() -> bool:
|
|
222
|
+
"""Write conduct-mcp into ~/.codex/config.toml (TOML — different format)."""
|
|
223
|
+
config_path = Path.home() / ".codex" / "config.toml"
|
|
255
224
|
if not config_path.parent.exists():
|
|
256
225
|
return False
|
|
257
226
|
try:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if "conduct" in servers:
|
|
261
|
-
return True
|
|
262
|
-
servers["conduct"] = {"command": "conduct-mcp", "args": []}
|
|
263
|
-
config_path.write_text(json.dumps(existing, indent=2))
|
|
264
|
-
return True
|
|
265
|
-
except Exception:
|
|
266
|
-
return False
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def _write_vscode_mcp_config() -> bool:
|
|
270
|
-
"""Write conduct-mcp into VS Code settings.json (mcp.servers). Returns True if written."""
|
|
271
|
-
# Check both standard locations
|
|
272
|
-
candidates = [
|
|
273
|
-
Path.home() / ".vscode" / "settings.json",
|
|
274
|
-
Path.home() / "Library" / "Application Support" / "Code" / "User" / "settings.json",
|
|
275
|
-
Path.home() / ".config" / "Code" / "User" / "settings.json",
|
|
276
|
-
]
|
|
277
|
-
settings_path = next((p for p in candidates if p.exists()), None)
|
|
278
|
-
if not settings_path:
|
|
279
|
-
return False
|
|
280
|
-
try:
|
|
281
|
-
existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
282
|
-
servers = existing.setdefault("mcp", {}).setdefault("servers", {})
|
|
283
|
-
if "conduct" in servers:
|
|
227
|
+
content = config_path.read_text() if config_path.exists() else ""
|
|
228
|
+
if "conduct-mcp" in content:
|
|
284
229
|
return True
|
|
285
|
-
|
|
286
|
-
settings_path.write_text(json.dumps(existing, indent=2))
|
|
230
|
+
config_path.write_text(content + '\n[mcp_servers.conduct]\ncommand = "conduct-mcp"\nargs = []\n')
|
|
287
231
|
return True
|
|
288
232
|
except Exception:
|
|
289
233
|
return False
|
|
@@ -427,30 +371,26 @@ def cmd_mcp_install(args):
|
|
|
427
371
|
import shutil
|
|
428
372
|
import subprocess
|
|
429
373
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
374
|
+
home = Path.home()
|
|
375
|
+
# claude mcp add --global writes to project-level settings; write directly instead.
|
|
376
|
+
_MCP_TARGETS = [
|
|
377
|
+
("Claude Code", home / ".claude" / "settings.json", ("mcpServers",), True),
|
|
378
|
+
("Cursor", home / ".cursor" / "mcp.json", ("mcpServers",), False),
|
|
379
|
+
("Windsurf", home / ".codeium" / "windsurf" / "mcp_config.json", ("mcpServers",), False),
|
|
380
|
+
("VS Code (Copilot)",next((p for p in [
|
|
381
|
+
home / ".vscode" / "settings.json",
|
|
382
|
+
home / "Library" / "Application Support" / "Code" / "User" / "settings.json",
|
|
383
|
+
home / ".config" / "Code" / "User" / "settings.json",
|
|
384
|
+
] if p.exists()), None), ("mcp", "servers"), False),
|
|
385
|
+
]
|
|
437
386
|
|
|
438
|
-
|
|
387
|
+
registered = []
|
|
388
|
+
for label, path, keys, create in _MCP_TARGETS:
|
|
389
|
+
if path and _write_mcp_config(path, keys=keys, create=create):
|
|
390
|
+
registered.append(label)
|
|
439
391
|
if _write_codex_mcp_config():
|
|
440
392
|
registered.append("Codex")
|
|
441
393
|
|
|
442
|
-
# --- Cursor ---
|
|
443
|
-
if _write_cursor_mcp_config():
|
|
444
|
-
registered.append("Cursor")
|
|
445
|
-
|
|
446
|
-
# --- Windsurf ---
|
|
447
|
-
if _write_windsurf_mcp_config():
|
|
448
|
-
registered.append("Windsurf")
|
|
449
|
-
|
|
450
|
-
# --- VS Code (Copilot) ---
|
|
451
|
-
if _write_vscode_mcp_config():
|
|
452
|
-
registered.append("VS Code (Copilot)")
|
|
453
|
-
|
|
454
394
|
if registered:
|
|
455
395
|
print(f"{GREEN}✓ conduct-mcp registered in: {', '.join(registered)}{RESET}")
|
|
456
396
|
print(f"{GRAY} Restart your AI tools to pick up the new MCP server.{RESET}")
|
|
@@ -676,47 +616,38 @@ def cmd_test(args):
|
|
|
676
616
|
})
|
|
677
617
|
return payload
|
|
678
618
|
|
|
679
|
-
|
|
680
|
-
_run_tests_parallel(server, workspace_id, api_key, token, hdrs, targets, _build_payload)
|
|
681
|
-
else:
|
|
682
|
-
_run_tests_serial(server, workspace_id, api_key, token, hdrs, targets, _build_payload)
|
|
619
|
+
_run_tests(server, workspace_id, api_key, token, hdrs, targets, _build_payload, parallel=parallel)
|
|
683
620
|
|
|
684
621
|
|
|
685
|
-
def
|
|
686
|
-
|
|
687
|
-
for wf in targets:
|
|
688
|
-
name = wf["name"]
|
|
689
|
-
wf_id = str(wf["id"])
|
|
690
|
-
slug = wf.get("playbook_slug", "")
|
|
622
|
+
def _run_tests(server, workspace_id, api_key, token, hdrs, targets, build_payload, *, parallel: bool = False):
|
|
623
|
+
import threading
|
|
691
624
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
625
|
+
if not parallel:
|
|
626
|
+
results = []
|
|
627
|
+
for wf in targets:
|
|
628
|
+
name = wf["name"]
|
|
629
|
+
wf_id = str(wf["id"])
|
|
630
|
+
slug = wf.get("playbook_slug", "")
|
|
631
|
+
print(f"{CYAN}── {name}{RESET} {GRAY}({slug}){RESET}")
|
|
632
|
+
try:
|
|
633
|
+
run = api.req("POST", f"{server}/workflows/{wf_id}/trigger", hdrs, build_payload(slug))
|
|
634
|
+
except SystemExit:
|
|
635
|
+
results.append((name, False, None))
|
|
636
|
+
print()
|
|
637
|
+
continue
|
|
638
|
+
run_id = run.get("run_id")
|
|
639
|
+
print(f" {GRAY}run: {run_id}{RESET}")
|
|
640
|
+
try:
|
|
641
|
+
ok = _stream_run(server, wf_id, run_id, workspace_id, token, api_key)
|
|
642
|
+
except Exception:
|
|
643
|
+
ok = _poll_run(server, wf_id, run_id, hdrs)
|
|
644
|
+
results.append((name, ok, run_id))
|
|
697
645
|
print()
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
run_id = run.get("run_id")
|
|
701
|
-
print(f" {GRAY}run: {run_id}{RESET}")
|
|
702
|
-
|
|
703
|
-
try:
|
|
704
|
-
ok = _stream_run(server, wf_id, run_id, workspace_id, token, api_key)
|
|
705
|
-
except Exception:
|
|
706
|
-
ok = _poll_run(server, wf_id, run_id, hdrs)
|
|
707
|
-
|
|
708
|
-
results.append((name, ok, run_id))
|
|
709
|
-
print()
|
|
710
|
-
|
|
711
|
-
_print_results(results)
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
def _run_tests_parallel(server, workspace_id, api_key, token, hdrs, targets, build_payload):
|
|
715
|
-
"""Fire all triggers immediately, then poll all runs concurrently."""
|
|
716
|
-
import threading
|
|
646
|
+
_print_results(results)
|
|
647
|
+
return
|
|
717
648
|
|
|
718
|
-
#
|
|
719
|
-
pending = []
|
|
649
|
+
# parallel: fire all triggers, then poll concurrently
|
|
650
|
+
pending = []
|
|
720
651
|
for wf in targets:
|
|
721
652
|
name = wf["name"]
|
|
722
653
|
wf_id = str(wf["id"])
|
|
@@ -731,7 +662,6 @@ def _run_tests_parallel(server, workspace_id, api_key, token, hdrs, targets, bui
|
|
|
731
662
|
pending.append((name, wf_id, None))
|
|
732
663
|
|
|
733
664
|
print(f"\n Polling {len(pending)} runs concurrently…\n")
|
|
734
|
-
|
|
735
665
|
results_lock = threading.Lock()
|
|
736
666
|
results: list = [None] * len(pending)
|
|
737
667
|
|
|
@@ -754,7 +684,6 @@ def _run_tests_parallel(server, workspace_id, api_key, token, hdrs, targets, bui
|
|
|
754
684
|
t.start()
|
|
755
685
|
for t in threads:
|
|
756
686
|
t.join()
|
|
757
|
-
|
|
758
687
|
_print_results(results)
|
|
759
688
|
|
|
760
689
|
|
|
@@ -3,9 +3,31 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import threading
|
|
6
|
+
import time
|
|
6
7
|
import urllib.request
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
10
|
+
_FLUSH_INTERVAL = 8 * 3600 # 8 hours in seconds
|
|
11
|
+
_FLUSH_STAMP = Path.home() / ".conduct" / "last_memory_flush"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def should_periodic_flush() -> bool:
|
|
15
|
+
"""True if 8+ hours have passed since the last periodic memory flush."""
|
|
16
|
+
try:
|
|
17
|
+
if not _FLUSH_STAMP.exists():
|
|
18
|
+
return True
|
|
19
|
+
return time.time() - float(_FLUSH_STAMP.read_text().strip()) >= _FLUSH_INTERVAL
|
|
20
|
+
except Exception:
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def mark_flushed() -> None:
|
|
25
|
+
try:
|
|
26
|
+
_FLUSH_STAMP.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
_FLUSH_STAMP.write_text(str(time.time()))
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
9
31
|
|
|
10
32
|
def _load_config():
|
|
11
33
|
cfg_path = Path.home() / ".conduct" / "config.json"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|