conduct-cli 0.4.70__tar.gz → 0.4.72__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.70 → conduct_cli-0.4.72}/PKG-INFO +4 -1
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/README.md +2 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/pyproject.toml +2 -2
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/guard.py +139 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/hook_session_start_template.py +23 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/hook_template.py +24 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/main.py +32 -0
- conduct_cli-0.4.72/src/conduct_cli/memory.py +96 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli.egg-info/PKG-INFO +4 -1
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
- conduct_cli-0.4.72/src/conduct_cli.egg-info/requires.txt +3 -0
- conduct_cli-0.4.70/src/conduct_cli.egg-info/requires.txt +0 -2
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/setup.cfg +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/setup.py +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/hook_precompact_template.py +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.4.70 → conduct_cli-0.4.72}/tests/test_switch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conduct-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.72
|
|
4
4
|
Summary: CLI for Conduct AI — install agents, manage projects, run tests
|
|
5
5
|
Author-email: Conduct AI <hello@conductai.ai>
|
|
6
6
|
License: MIT
|
|
@@ -22,11 +22,14 @@ Requires-Python: >=3.9
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: pyyaml>=6.0
|
|
24
24
|
Requires-Dist: rich>=13.0
|
|
25
|
+
Requires-Dist: agent-booster[watch]>=0.2.23
|
|
25
26
|
|
|
26
27
|
# conduct-cli
|
|
27
28
|
|
|
28
29
|
Official CLI for [Conduct AI](https://conductai.ai) — install AI agents, manage projects, run end-to-end tests, and enforce team AI policies with ConductGuard.
|
|
29
30
|
|
|
31
|
+

|
|
32
|
+
|
|
30
33
|
## Install
|
|
31
34
|
|
|
32
35
|
```bash
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Official CLI for [Conduct AI](https://conductai.ai) — install AI agents, manage projects, run end-to-end tests, and enforce team AI policies with ConductGuard.
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conduct-cli"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.72"
|
|
8
8
|
description = "CLI for Conduct AI — install agents, manage projects, run tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Programming Language :: Python :: 3.12",
|
|
24
24
|
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
25
25
|
]
|
|
26
|
-
dependencies = ["pyyaml>=6.0", "rich>=13.0"]
|
|
26
|
+
dependencies = ["pyyaml>=6.0", "rich>=13.0", "agent-booster[watch]>=0.2.23"]
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
29
|
Homepage = "https://conductai.ai"
|
|
@@ -740,6 +740,9 @@ def cmd_guard_sync(args):
|
|
|
740
740
|
pass
|
|
741
741
|
print(f" {GREEN}Hook script updated{RESET}")
|
|
742
742
|
|
|
743
|
+
# Auto-init Agent Booster if installed but not yet set up in this project
|
|
744
|
+
_ensure_booster(Path.cwd())
|
|
745
|
+
|
|
743
746
|
# Capture savings from RTK and Agent Booster
|
|
744
747
|
_report_savings(cfg, base_url, api_key)
|
|
745
748
|
|
|
@@ -752,6 +755,48 @@ def cmd_guard_sync(args):
|
|
|
752
755
|
print(f"\n{BOLD}Policy refreshed ({rule_count} rule(s)).{RESET}")
|
|
753
756
|
|
|
754
757
|
|
|
758
|
+
def _ensure_booster(root: Path) -> None:
|
|
759
|
+
"""Auto-init and background-index booster if installed but not yet set up."""
|
|
760
|
+
import shutil
|
|
761
|
+
import subprocess
|
|
762
|
+
|
|
763
|
+
if not shutil.which("booster"):
|
|
764
|
+
return # not installed — conduct-cli 0.4.71+ installs it, but may not be on PATH yet
|
|
765
|
+
|
|
766
|
+
db_path = root / ".booster" / "symbols.db"
|
|
767
|
+
hooks_path = root / ".claude" / "hooks" / "booster-gate.py"
|
|
768
|
+
|
|
769
|
+
# Init (writes hook scripts + wires settings.json) — fast, idempotent
|
|
770
|
+
if not hooks_path.exists():
|
|
771
|
+
try:
|
|
772
|
+
subprocess.run(["booster", "init", "--yes"], capture_output=True, timeout=15, cwd=str(root))
|
|
773
|
+
print(f" {GREEN}Agent Booster:{RESET} hooks installed")
|
|
774
|
+
except Exception:
|
|
775
|
+
return
|
|
776
|
+
|
|
777
|
+
# Index in background — may take 10-60s on large repos, never blocks sync
|
|
778
|
+
if not db_path.exists():
|
|
779
|
+
try:
|
|
780
|
+
subprocess.Popen(
|
|
781
|
+
["booster", "index", "--embed"],
|
|
782
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
783
|
+
cwd=str(root),
|
|
784
|
+
)
|
|
785
|
+
print(f" {GREEN}Agent Booster:{RESET} indexing in background (Read/Grep intercept active shortly)")
|
|
786
|
+
except Exception:
|
|
787
|
+
pass
|
|
788
|
+
else:
|
|
789
|
+
symbols_count = 0
|
|
790
|
+
try:
|
|
791
|
+
import sqlite3
|
|
792
|
+
conn = sqlite3.connect(str(db_path))
|
|
793
|
+
symbols_count = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
|
|
794
|
+
conn.close()
|
|
795
|
+
except Exception:
|
|
796
|
+
pass
|
|
797
|
+
print(f" {GREEN}Agent Booster:{RESET} {symbols_count} symbols indexed — Read/Grep intercept active")
|
|
798
|
+
|
|
799
|
+
|
|
755
800
|
def _report_savings(cfg: dict, base_url: str, api_key: str) -> None:
|
|
756
801
|
import subprocess
|
|
757
802
|
|
|
@@ -1054,9 +1099,101 @@ def register_guard_parser(sub):
|
|
|
1054
1099
|
help="Time window: 1h, 24h, 7d, 30d (default: 24h)",
|
|
1055
1100
|
)
|
|
1056
1101
|
|
|
1102
|
+
# conduct guard booster-status
|
|
1103
|
+
guard_sub.add_parser("booster-status", help="Verify Agent Booster intercept is active for this project")
|
|
1104
|
+
|
|
1057
1105
|
return guard_p, guard_sub
|
|
1058
1106
|
|
|
1059
1107
|
|
|
1108
|
+
def cmd_guard_booster_status(args):
|
|
1109
|
+
"""Show whether booster is intercepting Read/Grep in this project."""
|
|
1110
|
+
import shutil, sqlite3, subprocess
|
|
1111
|
+
|
|
1112
|
+
root = Path.cwd()
|
|
1113
|
+
db_path = root / ".booster" / "symbols.db"
|
|
1114
|
+
hooks_path = root / ".claude" / "hooks" / "booster-gate.py"
|
|
1115
|
+
settings_p = root / ".claude" / "settings.json"
|
|
1116
|
+
|
|
1117
|
+
booster_bin = shutil.which("booster")
|
|
1118
|
+
print(f"\n{BOLD}Agent Booster intercept status — {root.name}{RESET}\n")
|
|
1119
|
+
|
|
1120
|
+
# 1. Binary
|
|
1121
|
+
if booster_bin:
|
|
1122
|
+
print(f" {GREEN}✓{RESET} booster installed ({booster_bin})")
|
|
1123
|
+
else:
|
|
1124
|
+
print(f" {RED}✗{RESET} booster not found on PATH — run: pip install agent-booster")
|
|
1125
|
+
return
|
|
1126
|
+
|
|
1127
|
+
# 2. Hook scripts written
|
|
1128
|
+
if hooks_path.exists():
|
|
1129
|
+
print(f" {GREEN}✓{RESET} hook scripts present (.claude/hooks/booster-gate.py)")
|
|
1130
|
+
else:
|
|
1131
|
+
print(f" {RED}✗{RESET} hook scripts missing — run: conduct guard sync")
|
|
1132
|
+
|
|
1133
|
+
# 3. Wired in settings.json
|
|
1134
|
+
wired = False
|
|
1135
|
+
if settings_p.exists():
|
|
1136
|
+
import json as _json
|
|
1137
|
+
s = _json.loads(settings_p.read_text())
|
|
1138
|
+
for h in s.get("hooks", {}).get("PreToolUse", []):
|
|
1139
|
+
if h.get("matcher") == "Read":
|
|
1140
|
+
wired = True
|
|
1141
|
+
break
|
|
1142
|
+
if wired:
|
|
1143
|
+
print(f" {GREEN}✓{RESET} Read hook wired in .claude/settings.json")
|
|
1144
|
+
else:
|
|
1145
|
+
print(f" {RED}✗{RESET} Read hook NOT in .claude/settings.json — run: conduct guard sync")
|
|
1146
|
+
|
|
1147
|
+
# 4. Index
|
|
1148
|
+
if db_path.exists():
|
|
1149
|
+
try:
|
|
1150
|
+
conn = sqlite3.connect(str(db_path))
|
|
1151
|
+
count = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
|
|
1152
|
+
files = conn.execute("SELECT COUNT(DISTINCT file) FROM symbols").fetchone()[0]
|
|
1153
|
+
conn.close()
|
|
1154
|
+
print(f" {GREEN}✓{RESET} symbols.db — {count} symbols across {files} files")
|
|
1155
|
+
except Exception:
|
|
1156
|
+
print(f" {YELLOW}?{RESET} symbols.db exists but could not be read")
|
|
1157
|
+
else:
|
|
1158
|
+
print(f" {RED}✗{RESET} symbols.db missing — Read calls fall through unintercepted")
|
|
1159
|
+
print(f" run: booster index --embed (or: conduct guard sync to trigger it)")
|
|
1160
|
+
return
|
|
1161
|
+
|
|
1162
|
+
# 5. Live intercept test — try reading a known file and check if smart-read fires
|
|
1163
|
+
print(f"\n {BOLD}Live intercept test:{RESET}")
|
|
1164
|
+
try:
|
|
1165
|
+
import tempfile, json as _json
|
|
1166
|
+
# Pick the first indexed file
|
|
1167
|
+
conn = sqlite3.connect(str(db_path))
|
|
1168
|
+
row = conn.execute("SELECT file FROM symbols LIMIT 1").fetchone()
|
|
1169
|
+
conn.close()
|
|
1170
|
+
if row:
|
|
1171
|
+
# Prefer a .py/.ts file — more likely to have symbols and trigger smart-read
|
|
1172
|
+
conn = sqlite3.connect(str(db_path))
|
|
1173
|
+
src = conn.execute(
|
|
1174
|
+
"SELECT file FROM symbols WHERE file LIKE '%.py' OR file LIKE '%.ts' LIMIT 1"
|
|
1175
|
+
).fetchone() or row
|
|
1176
|
+
conn.close()
|
|
1177
|
+
test_file = str(root / src[0])
|
|
1178
|
+
payload = _json.dumps({"tool_name": "Read", "tool_input": {"file_path": test_file}})
|
|
1179
|
+
r = subprocess.run(
|
|
1180
|
+
["python3", str(hooks_path)],
|
|
1181
|
+
input=payload, capture_output=True, text=True, timeout=10,
|
|
1182
|
+
)
|
|
1183
|
+
if r.returncode == 2:
|
|
1184
|
+
lines = r.stdout.strip().splitlines()
|
|
1185
|
+
print(f" {GREEN}✓{RESET} Read intercepted → smart-read fired ({len(lines)} lines returned)")
|
|
1186
|
+
print(f" tested on: {row[0]}")
|
|
1187
|
+
elif r.returncode == 0:
|
|
1188
|
+
print(f" {YELLOW}~{RESET} Hook ran but passed through (file may not have symbols)")
|
|
1189
|
+
else:
|
|
1190
|
+
print(f" {RED}✗{RESET} Hook errored (exit {r.returncode})")
|
|
1191
|
+
except Exception as e:
|
|
1192
|
+
print(f" {YELLOW}?{RESET} Could not run live test: {e}")
|
|
1193
|
+
|
|
1194
|
+
print()
|
|
1195
|
+
|
|
1196
|
+
|
|
1060
1197
|
def dispatch_guard(args, guard_p):
|
|
1061
1198
|
"""Dispatch to the correct guard handler. Called from main()."""
|
|
1062
1199
|
guard_command = getattr(args, "guard_command", None)
|
|
@@ -1068,6 +1205,8 @@ def dispatch_guard(args, guard_p):
|
|
|
1068
1205
|
cmd_guard_savings(args)
|
|
1069
1206
|
elif guard_command == "audit":
|
|
1070
1207
|
cmd_guard_audit(args)
|
|
1208
|
+
elif guard_command == "booster-status":
|
|
1209
|
+
cmd_guard_booster_status(args)
|
|
1071
1210
|
else:
|
|
1072
1211
|
guard_p.print_help()
|
|
1073
1212
|
sys.exit(1)
|
|
@@ -46,6 +46,29 @@ def main():
|
|
|
46
46
|
else:
|
|
47
47
|
lines.append("- Memory index:\n (none)")
|
|
48
48
|
|
|
49
|
+
# Inject relevant team memories for the current repo
|
|
50
|
+
try:
|
|
51
|
+
repo = None
|
|
52
|
+
try:
|
|
53
|
+
import subprocess
|
|
54
|
+
out = subprocess.check_output(["git", "remote", "get-url", "origin"],
|
|
55
|
+
stderr=subprocess.DEVNULL, text=True).strip()
|
|
56
|
+
if "github.com" in out:
|
|
57
|
+
repo = out.split("github.com")[-1].lstrip("/:").rstrip(".git")
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
from conduct_cli.memory import search_team_memory
|
|
62
|
+
results = search_team_memory("recent learnings patterns bugs", repo=repo, limit=3)
|
|
63
|
+
if results:
|
|
64
|
+
lines.append("- Team knowledge:")
|
|
65
|
+
for r in results[:3]:
|
|
66
|
+
dev = r.get("developer_id", "teammate")[:8]
|
|
67
|
+
summary = r.get("summary", "")[:120]
|
|
68
|
+
lines.append(f" {dev}: {summary}")
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
49
72
|
print("\n".join(lines))
|
|
50
73
|
except Exception:
|
|
51
74
|
pass
|
|
@@ -131,6 +131,20 @@ except Exception:
|
|
|
131
131
|
return None, "allow", None, None
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
def _detect_repo() -> str | None:
|
|
135
|
+
try:
|
|
136
|
+
import subprocess
|
|
137
|
+
out = subprocess.check_output(["git", "remote", "get-url", "origin"],
|
|
138
|
+
stderr=subprocess.DEVNULL, text=True).strip()
|
|
139
|
+
# github.com/owner/repo or git@github.com:owner/repo
|
|
140
|
+
if "github.com" in out:
|
|
141
|
+
parts = out.split("github.com")[-1].lstrip("/:").rstrip(".git")
|
|
142
|
+
return parts # owner/repo
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
134
148
|
def _detect_ai_tool():
|
|
135
149
|
import os
|
|
136
150
|
if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_CODE_ENTRYPOINT"):
|
|
@@ -535,6 +549,16 @@ def main():
|
|
|
535
549
|
except Exception:
|
|
536
550
|
sys.exit(0)
|
|
537
551
|
|
|
552
|
+
# Stop hook — session ended, capture for team memory
|
|
553
|
+
if data.get("hook_event_name") == "Stop" or data.get("stop_hook_active"):
|
|
554
|
+
session_id = data.get("session_id", "")
|
|
555
|
+
transcript_path = data.get("transcript_path")
|
|
556
|
+
# Detect repo from CWD git remote
|
|
557
|
+
repo = _detect_repo()
|
|
558
|
+
from conduct_cli.memory import post_session_to_api
|
|
559
|
+
post_session_to_api(session_id, transcript_path, repo)
|
|
560
|
+
sys.exit(0)
|
|
561
|
+
|
|
538
562
|
# Policy version check (cached 60s) — auto-syncs if server version differs
|
|
539
563
|
_maybe_sync_policy()
|
|
540
564
|
|
|
@@ -2708,6 +2708,28 @@ def cmd_test_guard(args):
|
|
|
2708
2708
|
print(f"\n {CYAN}→ View events: {api_url.replace('api.', 'app.')}/guard/activity{RESET}\n")
|
|
2709
2709
|
|
|
2710
2710
|
|
|
2711
|
+
def cmd_memory(args):
|
|
2712
|
+
from conduct_cli.memory import search_team_memory
|
|
2713
|
+
memory_command = getattr(args, "memory_command", None)
|
|
2714
|
+
if memory_command == "search":
|
|
2715
|
+
query = " ".join(args.query)
|
|
2716
|
+
results = search_team_memory(query, repo=getattr(args, "repo", None), limit=args.limit)
|
|
2717
|
+
if not results:
|
|
2718
|
+
print("No team memories found.")
|
|
2719
|
+
return
|
|
2720
|
+
for r in results:
|
|
2721
|
+
repo = r.get("repo_full_name", "unknown")
|
|
2722
|
+
summary = r.get("summary", "")
|
|
2723
|
+
tags = ", ".join(r.get("topic_tags") or [])
|
|
2724
|
+
created = r.get("created_at", "")[:10]
|
|
2725
|
+
print(f"\n{BOLD}{repo}{RESET} {GRAY}{created}{RESET}")
|
|
2726
|
+
if tags:
|
|
2727
|
+
print(f" Tags: {tags}")
|
|
2728
|
+
print(f" {summary}")
|
|
2729
|
+
else:
|
|
2730
|
+
print("Usage: conduct memory search <query>")
|
|
2731
|
+
|
|
2732
|
+
|
|
2711
2733
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
2712
2734
|
|
|
2713
2735
|
def main():
|
|
@@ -2860,6 +2882,14 @@ def main():
|
|
|
2860
2882
|
sr_p = sub.add_parser("session-report", help="Analyse local AI coding sessions with paxel and send report to admin")
|
|
2861
2883
|
sr_p.add_argument("--developer", default=None, help="Developer name (defaults to OS username)")
|
|
2862
2884
|
|
|
2885
|
+
# conduct memory
|
|
2886
|
+
memory_p = sub.add_parser("memory", help="Search team session memories")
|
|
2887
|
+
memory_sub = memory_p.add_subparsers(dest="memory_command")
|
|
2888
|
+
mem_search_p = memory_sub.add_parser("search", help="Search team memories")
|
|
2889
|
+
mem_search_p.add_argument("query", nargs="+", help="Search query")
|
|
2890
|
+
mem_search_p.add_argument("--repo", help="Filter by repo (owner/repo)")
|
|
2891
|
+
mem_search_p.add_argument("--limit", type=int, default=5, help="Max results")
|
|
2892
|
+
|
|
2863
2893
|
args = parser.parse_args()
|
|
2864
2894
|
|
|
2865
2895
|
if args.command == "login":
|
|
@@ -2937,6 +2967,8 @@ def main():
|
|
|
2937
2967
|
cmd_test_security_verify(args)
|
|
2938
2968
|
elif args.command == "session-report":
|
|
2939
2969
|
cmd_session_report(args)
|
|
2970
|
+
elif args.command == "memory":
|
|
2971
|
+
cmd_memory(args)
|
|
2940
2972
|
else:
|
|
2941
2973
|
parser.print_help()
|
|
2942
2974
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Team session memory helpers for Conduct CLI."""
|
|
2
|
+
import json
|
|
3
|
+
import threading
|
|
4
|
+
import urllib.request
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _load_config():
|
|
9
|
+
cfg_path = Path.home() / ".conduct" / "config.json"
|
|
10
|
+
if not cfg_path.exists():
|
|
11
|
+
return None
|
|
12
|
+
try:
|
|
13
|
+
return json.loads(cfg_path.read_text())
|
|
14
|
+
except Exception:
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def post_session_to_api(session_id: str, transcript_path: str | None, repo: str | None) -> bool:
|
|
19
|
+
"""Fire-and-forget POST to /team-memory/sessions. Returns True if thread started."""
|
|
20
|
+
cfg = _load_config()
|
|
21
|
+
if not cfg:
|
|
22
|
+
return False
|
|
23
|
+
server = cfg.get("server", "").rstrip("/")
|
|
24
|
+
api_key = cfg.get("api_key", "")
|
|
25
|
+
workspace_id = cfg.get("workspace_id", "")
|
|
26
|
+
if not server or not workspace_id:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
raw_transcript = None
|
|
30
|
+
if transcript_path:
|
|
31
|
+
try:
|
|
32
|
+
text = Path(transcript_path).read_text(errors="ignore")
|
|
33
|
+
raw_transcript = text[:12000]
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
payload = json.dumps({
|
|
38
|
+
"session_id": session_id,
|
|
39
|
+
"tool": "claude_code",
|
|
40
|
+
"repo_full_name": repo,
|
|
41
|
+
"raw_transcript": raw_transcript,
|
|
42
|
+
"files_touched": [],
|
|
43
|
+
}).encode()
|
|
44
|
+
|
|
45
|
+
def _send():
|
|
46
|
+
try:
|
|
47
|
+
req = urllib.request.Request(
|
|
48
|
+
f"{server}/team-memory/sessions",
|
|
49
|
+
data=payload,
|
|
50
|
+
headers={
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
"X-API-Key": api_key,
|
|
53
|
+
"X-Workspace-ID": workspace_id,
|
|
54
|
+
},
|
|
55
|
+
method="POST",
|
|
56
|
+
)
|
|
57
|
+
urllib.request.urlopen(req, timeout=10)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
t = threading.Thread(target=_send, daemon=True)
|
|
62
|
+
t.start()
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def search_team_memory(query: str, repo: str | None = None, limit: int = 5) -> list[dict]:
|
|
67
|
+
"""Search team session memories. Returns list of result dicts, never raises."""
|
|
68
|
+
cfg = _load_config()
|
|
69
|
+
if not cfg:
|
|
70
|
+
return []
|
|
71
|
+
server = cfg.get("server", "").rstrip("/")
|
|
72
|
+
api_key = cfg.get("api_key", "")
|
|
73
|
+
workspace_id = cfg.get("workspace_id", "")
|
|
74
|
+
if not server or not workspace_id:
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
import urllib.parse
|
|
79
|
+
params = {"q": query, "limit": str(limit)}
|
|
80
|
+
if repo:
|
|
81
|
+
params["repo"] = repo
|
|
82
|
+
url = f"{server}/team-memory/search?{urllib.parse.urlencode(params)}"
|
|
83
|
+
req = urllib.request.Request(
|
|
84
|
+
url,
|
|
85
|
+
headers={
|
|
86
|
+
"X-API-Key": api_key,
|
|
87
|
+
"X-Workspace-ID": workspace_id,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
91
|
+
data = json.loads(resp.read())
|
|
92
|
+
if isinstance(data, list):
|
|
93
|
+
return data
|
|
94
|
+
return data.get("results", []) if isinstance(data, dict) else []
|
|
95
|
+
except Exception:
|
|
96
|
+
return []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conduct-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.72
|
|
4
4
|
Summary: CLI for Conduct AI — install agents, manage projects, run tests
|
|
5
5
|
Author-email: Conduct AI <hello@conductai.ai>
|
|
6
6
|
License: MIT
|
|
@@ -22,11 +22,14 @@ Requires-Python: >=3.9
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: pyyaml>=6.0
|
|
24
24
|
Requires-Dist: rich>=13.0
|
|
25
|
+
Requires-Dist: agent-booster[watch]>=0.2.23
|
|
25
26
|
|
|
26
27
|
# conduct-cli
|
|
27
28
|
|
|
28
29
|
Official CLI for [Conduct AI](https://conductai.ai) — install AI agents, manage projects, run end-to-end tests, and enforce team AI policies with ConductGuard.
|
|
29
30
|
|
|
31
|
+

|
|
32
|
+
|
|
30
33
|
## Install
|
|
31
34
|
|
|
32
35
|
```bash
|
|
@@ -10,6 +10,7 @@ src/conduct_cli/hook_session_start_template.py
|
|
|
10
10
|
src/conduct_cli/hook_template.py
|
|
11
11
|
src/conduct_cli/main.py
|
|
12
12
|
src/conduct_cli/mcp_server.py
|
|
13
|
+
src/conduct_cli/memory.py
|
|
13
14
|
src/conduct_cli.egg-info/PKG-INFO
|
|
14
15
|
src/conduct_cli.egg-info/SOURCES.txt
|
|
15
16
|
src/conduct_cli.egg-info/dependency_links.txt
|
|
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
|