codeer-cli 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

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.
codeer_cli/cli.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  codeer check
4
4
  codeer agent list|get|apply|diff|versions
5
- codeer kb list|files|upload
5
+ codeer kb list|files|upload|faq-list|faq-get|faq-create|faq-update|faq-delete
6
6
  codeer eval list|evaluators|evaluator-create|evaluator-update|run|export|reconcile|cases-apply|rubrics|rubrics-apply
7
7
  codeer history list|get|conversations|negative-feedback
8
8
  """
@@ -18,7 +18,31 @@ from .commands import check
18
18
 
19
19
 
20
20
  def main(argv: list[str] | None = None) -> int:
21
- parser = argparse.ArgumentParser(prog="codeer")
21
+ parser = argparse.ArgumentParser(
22
+ prog="codeer",
23
+ formatter_class=argparse.RawDescriptionHelpFormatter,
24
+ description="Codeer CLI — self-describing agent lifecycle tools.",
25
+ epilog="""\
26
+ Safe workflow for coding agents:
27
+ codeer check --json
28
+ codeer agent list
29
+ codeer agent get <agent-id> --full
30
+ codeer kb list
31
+ codeer eval list --agent <agent-id>
32
+ codeer eval evaluators
33
+ codeer agent diff --agent <agent-id> --from-version <n> --to-version <n>
34
+ codeer eval reconcile --agent <agent-id> --manifest .codeer/eval_cases.json
35
+
36
+ Preview mutations before applying:
37
+ codeer agent apply --payload agent.json --dry-run
38
+ codeer eval cases-apply --agent <agent-id> --cases eval_cases.json --dry-run
39
+ codeer eval rubrics-apply --rubrics rubrics.json --dry-run
40
+ codeer kb upload --dir kb --name "Product KB" --dry-run
41
+ codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --dry-run
42
+
43
+ Use --out <path> for large raw artifacts; stdout defaults to compact summaries.
44
+ """,
45
+ )
22
46
  sub = parser.add_subparsers(dest="group")
23
47
 
24
48
  check.register(sub)
@@ -67,6 +91,16 @@ def main(argv: list[str] | None = None) -> int:
67
91
  client = CodeerClient.from_env()
68
92
  except AuthError as e:
69
93
  if args.group == "check":
94
+ if getattr(args, "json", False):
95
+ print(json.dumps({
96
+ "status": "fail",
97
+ "auth": {
98
+ "ok": False,
99
+ "error": str(e),
100
+ },
101
+ "next_step": "Configure CODEER_API_KEY or a codeer profile",
102
+ }, ensure_ascii=False, indent=2))
103
+ return 1
70
104
  print(f"FAIL Auth: {e}", file=sys.stderr)
71
105
  print(" Configure CODEER_API_KEY or a codeer profile", file=sys.stderr)
72
106
  return 1
@@ -1,6 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import sys
5
+ from pathlib import Path
6
+ from typing import Any
4
7
 
5
8
 
6
9
  def log(*args, **kwargs):
@@ -10,3 +13,54 @@ def log(*args, **kwargs):
10
13
  def truncate(text: str, n: int = 60) -> str:
11
14
  text = text.replace("\n", " ").strip()
12
15
  return text[:n] + "..." if len(text) > n else text
16
+
17
+
18
+ NOISY_KEYS = {
19
+ "avatar",
20
+ "brand",
21
+ "creator",
22
+ "default_organization_id",
23
+ "default_scopes",
24
+ "default_workspace_id",
25
+ "is_owner",
26
+ "members",
27
+ "my_permissions",
28
+ "owner",
29
+ "profile",
30
+ "source_creator",
31
+ "user_role",
32
+ "workspace_organization_map",
33
+ }
34
+
35
+
36
+ def strip_noisy_fields(value: Any) -> Any:
37
+ """Remove server/account metadata that is not useful for agent lifecycle work."""
38
+ if isinstance(value, list):
39
+ return [strip_noisy_fields(item) for item in value]
40
+ if not isinstance(value, dict):
41
+ return value
42
+
43
+ out = {}
44
+ for key, item in value.items():
45
+ if key in NOISY_KEYS:
46
+ continue
47
+ if key == "workspace" and isinstance(item, dict):
48
+ out[key] = {
49
+ k: item.get(k)
50
+ for k in ("id", "name", "organization_id")
51
+ if item.get(k) is not None
52
+ }
53
+ continue
54
+ out[key] = strip_noisy_fields(item)
55
+ return out
56
+
57
+
58
+ def print_json(value: Any) -> None:
59
+ print(json.dumps(value, ensure_ascii=False, indent=2, default=str))
60
+
61
+
62
+ def write_json(path: str | None, value: Any) -> None:
63
+ if not path:
64
+ return
65
+ Path(path).write_text(json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n")
66
+ log(f"wrote full detail to {path}")
@@ -2,33 +2,48 @@ from __future__ import annotations
2
2
 
3
3
  import difflib
4
4
  import json
5
- import sys
6
5
  from pathlib import Path
7
6
  from typing import Optional
8
7
 
9
8
  from .. import agents as agents_mod
10
9
  from ..client import CodeerClient
11
- from ._util import log
10
+ from ._util import log, print_json, strip_noisy_fields, truncate, write_json
12
11
 
13
12
 
14
13
  def register(subparsers):
15
- agent = subparsers.add_parser("agent", help="Agent CRUD, versioning, publishing")
14
+ agent = subparsers.add_parser("agent", help="Agent CRUD and versioning")
16
15
  sub = agent.add_subparsers(dest="action", required=True)
17
16
 
18
17
  # codeer agent list
19
- p = sub.add_parser("list", help="List agents in workspace")
18
+ p = sub.add_parser(
19
+ "list",
20
+ help="List agents in workspace. Defaults to a lifecycle summary safe for Codex/Claude context.",
21
+ )
22
+ p.add_argument("--full", action="store_true",
23
+ help="Print bounded detail instead of the default lifecycle summary.")
24
+ p.add_argument("--out", default=None,
25
+ help="Write stripped full server detail to this file; stdout stays compact.")
20
26
  p.set_defaults(func=run_list)
21
27
 
22
28
  # codeer agent get <id>
23
- p = sub.add_parser("get", help="Read agent details")
29
+ p = sub.add_parser(
30
+ "get",
31
+ help="Read one agent. Defaults to summary; use --full for prompt/tool detail or --out for an artifact.",
32
+ )
24
33
  p.add_argument("agent_id")
34
+ p.add_argument("--full", action="store_true",
35
+ help="Print stripped full agent config, including system_prompt and tools.")
36
+ p.add_argument("--out", default=None,
37
+ help="Write stripped full agent config to this file.")
25
38
  p.set_defaults(func=run_get)
26
39
 
27
40
  # codeer agent apply --payload agent.json
28
- p = sub.add_parser("apply", help="Create or update agent from JSON payload")
41
+ p = sub.add_parser("apply", help="Create or update agent from JSON payload; run --dry-run first")
29
42
  p.add_argument("--payload", required=True, help="Path to agent payload JSON")
30
43
  p.add_argument("--agent-id", default=None, help="If set, PUT (update). Else POST (create).")
31
44
  p.add_argument("--note", default="", help="version_note for PUT")
45
+ p.add_argument("--dry-run", action="store_true",
46
+ help="Validate payload and print intended mutation without writing server state.")
32
47
  p.add_argument("--out", default=None, help="Write result JSON to this file too")
33
48
  p.set_defaults(func=run_apply)
34
49
 
@@ -43,27 +58,104 @@ def register(subparsers):
43
58
  p.set_defaults(func=run_diff)
44
59
 
45
60
  # codeer agent versions --agent <id>
46
- p = sub.add_parser("versions", help="List version history for an agent")
61
+ p = sub.add_parser(
62
+ "versions",
63
+ help="List version history. Defaults to version metadata only; use --out for full snapshots.",
64
+ )
47
65
  p.add_argument("--agent", required=True)
66
+ p.add_argument("--full", action="store_true",
67
+ help="Add bounded prompt/tool size metadata; full snapshots still require --out.")
68
+ p.add_argument("--out", default=None,
69
+ help="Write stripped full version snapshots to this file; stdout stays compact.")
48
70
  p.set_defaults(func=run_versions)
49
71
 
50
72
 
73
+ def _tool_summary(tools: list[dict] | None) -> list[dict]:
74
+ out = []
75
+ for t in tools or []:
76
+ form = t.get("custom_form_schema") if isinstance(t.get("custom_form_schema"), dict) else {}
77
+ out.append({
78
+ "id": t.get("id"),
79
+ "type": t.get("type"),
80
+ "name": t.get("name"),
81
+ "knowledge_node_count": len(t.get("knowledge_node_ids") or []),
82
+ "form_title": form.get("title"),
83
+ "invocation_preview": truncate(t.get("invocation_instruction") or "", 160),
84
+ })
85
+ return out
86
+
87
+
88
+ def _agent_summary(agent: dict, *, full: bool = False) -> dict:
89
+ tools = agent.get("unified_tools") or agent.get("tools") or []
90
+ row = {
91
+ "id": agent.get("id"),
92
+ "name": agent.get("name"),
93
+ "workspace": {
94
+ "id": agent.get("workspace_id") or (agent.get("workspace") or {}).get("id"),
95
+ "name": (agent.get("workspace") or {}).get("name"),
96
+ },
97
+ "publish_state": agent.get("publish_state"),
98
+ "version": agent.get("version"),
99
+ "latest_version_number": agent.get("latest_version_number"),
100
+ "published_version_number": agent.get("published_version_number"),
101
+ "publish_history_id": agent.get("publish_history_id"),
102
+ "llm_model": agent.get("llm_model"),
103
+ "agent_type": agent.get("agent_type"),
104
+ "updated_at": agent.get("updated_at"),
105
+ "tool_count": len(tools),
106
+ "system_prompt_chars": len(agent.get("system_prompt") or ""),
107
+ }
108
+ if full:
109
+ row["description"] = agent.get("description") or ""
110
+ row["use_search"] = agent.get("use_search")
111
+ row["suggested_questions"] = agent.get("suggested_questions") or []
112
+ row["tools"] = _tool_summary(tools)
113
+ row["system_prompt_preview"] = truncate(agent.get("system_prompt") or "", 1200)
114
+ return row
115
+
116
+
51
117
 
52
118
  def run_list(args, client) -> int:
53
119
  ws, org = client.resolve_scope()
54
120
  result = agents_mod.list_all(client, workspace_id=ws, organization_id=org)
55
- print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
121
+ write_json(args.out, strip_noisy_fields(result))
122
+ print_json([_agent_summary(a, full=args.full) for a in result])
56
123
  return 0
57
124
 
58
125
 
59
126
  def run_get(args, client) -> int:
60
127
  result = agents_mod.get(client, args.agent_id)
61
- print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
128
+ full_result = strip_noisy_fields(result)
129
+ write_json(args.out, full_result)
130
+ print_json(full_result if args.full else _agent_summary(result))
62
131
  return 0
63
132
 
64
133
 
65
134
  def run_apply(args, client) -> int:
66
135
  body = json.loads(Path(args.payload).read_text())
136
+ missing = [field for field in ("name", "system_prompt") if not body.get(field)]
137
+ if missing:
138
+ log(f"error: payload missing required field(s): {', '.join(missing)}")
139
+ return 2
140
+
141
+ if args.dry_run:
142
+ operation = "update" if args.agent_id else "create"
143
+ result = {
144
+ "dry_run": True,
145
+ "operation": operation,
146
+ "agent_id": args.agent_id,
147
+ "payload": str(Path(args.payload)),
148
+ "name": body.get("name"),
149
+ "system_prompt_chars": len(body.get("system_prompt") or ""),
150
+ "tool_count": len(body.get("unified_tools") or []),
151
+ "use_search": body.get("use_search", False),
152
+ "llm_model": body.get("llm_model"),
153
+ "version_note": args.note if args.agent_id else None,
154
+ "would_write_server_state": True,
155
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
156
+ }
157
+ print_json(result)
158
+ return 0
67
159
 
68
160
  if args.agent_id:
69
161
  body.pop("workspace_id", None)
@@ -118,7 +210,22 @@ def run_apply(args, client) -> int:
118
210
 
119
211
  def run_versions(args, client) -> int:
120
212
  versions = agents_mod.list_versions(client, args.agent)
121
- print(json.dumps(versions, ensure_ascii=False, indent=2, default=str))
213
+ write_json(args.out, strip_noisy_fields(versions))
214
+ rows = []
215
+ for v in versions:
216
+ row = {
217
+ "id": v.get("id"),
218
+ "version_number": v.get("version_number"),
219
+ "status": v.get("status"),
220
+ "was_published": v.get("was_published"),
221
+ "version_note": v.get("version_note") or "",
222
+ "created_at": v.get("created_at"),
223
+ }
224
+ if args.full:
225
+ row["system_prompt_chars"] = len(v.get("system_prompt") or "")
226
+ row["tool_count"] = len(v.get("unified_tools") or v.get("tools") or [])
227
+ rows.append(row)
228
+ print_json(rows)
122
229
  return 0
123
230
 
124
231
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import sys
4
5
 
5
6
  from ..client import AuthError, CodeerError
@@ -7,25 +8,46 @@ from ..client import AuthError, CodeerError
7
8
 
8
9
  def register(subparsers):
9
10
  p = subparsers.add_parser("check", help="Validate auth, workspace, and agent config")
11
+ p.add_argument("--json", action="store_true", help="Print machine-readable setup status")
10
12
  p.set_defaults(func=run)
11
13
 
12
14
 
13
15
  def run(args, client) -> int:
14
16
  errors = []
17
+ report = {
18
+ "status": "ok",
19
+ "auth": {"ok": False},
20
+ "workspace": {"ok": False},
21
+ "organization": {"ok": False},
22
+ "agent": {"ok": False, "configured": False, "optional": True},
23
+ }
15
24
 
16
25
  try:
17
26
  me = client.get_me()
18
27
  except AuthError:
28
+ if args.json:
29
+ report["status"] = "fail"
30
+ report["auth"]["error"] = "API key missing, invalid, expired, or revoked (401/403)"
31
+ report["next_step"] = "Create an admin workspace API key and export CODEER_API_KEY before running codeer"
32
+ print(json.dumps(report, ensure_ascii=False, indent=2))
33
+ return 1
19
34
  print("FAIL Auth: API key missing, invalid, expired, or revoked (401/403)", file=sys.stderr)
20
35
  print(" Create an admin workspace API key and export CODEER_API_KEY before running codeer", file=sys.stderr)
21
36
  return 1
22
37
  except CodeerError as e:
38
+ if args.json:
39
+ report["status"] = "fail"
40
+ report["auth"]["error"] = str(e)
41
+ print(json.dumps(report, ensure_ascii=False, indent=2))
42
+ return 1
23
43
  print(f"FAIL Auth: {e}", file=sys.stderr)
24
44
  return 1
25
45
 
26
46
  profile = me.get("profile", {})
27
47
  email = me.get("email") or "(unknown)"
28
- print(f" OK Auth: logged in as {email}")
48
+ report["auth"] = {"ok": True, "email": email}
49
+ if not args.json:
50
+ print(f" OK Auth: logged in as {email}")
29
51
 
30
52
  try:
31
53
  ws_id, org_id = client.resolve_scope()
@@ -35,26 +57,45 @@ def run(args, client) -> int:
35
57
 
36
58
  if ws_id:
37
59
  ws_name = _workspace_name(profile, ws_id)
60
+ report["workspace"] = {"ok": True, "id": ws_id, "name": ws_name}
38
61
  if ws_name:
39
- print(f" OK Workspace: {ws_name} ({ws_id})")
62
+ if not args.json:
63
+ print(f" OK Workspace: {ws_name} ({ws_id})")
40
64
  else:
41
- print(f" OK Workspace: {ws_id}")
65
+ if not args.json:
66
+ print(f" OK Workspace: {ws_id}")
42
67
 
43
68
  if org_id:
44
- print(f" OK Organization: {org_id}")
69
+ report["organization"] = {"ok": True, "id": org_id}
70
+ if not args.json:
71
+ print(f" OK Organization: {org_id}")
45
72
 
46
73
  agent_id = client.agent_id
47
74
  if agent_id:
75
+ report["agent"] = {"ok": False, "configured": True, "optional": True, "id": agent_id}
48
76
  try:
49
77
  agent = client.get(f"/external/agents/{agent_id}")
50
- print(f" OK Agent: {agent.get('name', '(unnamed)')} ({agent_id})")
78
+ report["agent"].update({"ok": True, "name": agent.get("name", "(unnamed)")})
79
+ if not args.json:
80
+ print(f" OK Agent: {agent.get('name', '(unnamed)')} ({agent_id})")
51
81
  except CodeerError:
52
82
  errors.append(f"WARN Agent: ID {agent_id} could not be read (may be in a different workspace)")
53
83
  else:
54
- print(" -- Agent: CODEER_AGENT_ID not set (optional)")
55
-
56
- for err in errors:
57
- print(err, file=sys.stderr)
84
+ if not args.json:
85
+ print(" -- Agent: CODEER_AGENT_ID not set (optional)")
86
+
87
+ if errors:
88
+ report["messages"] = errors
89
+ if any(e.startswith("FAIL") for e in errors):
90
+ report["status"] = "fail"
91
+ elif report["status"] == "ok":
92
+ report["status"] = "warn"
93
+
94
+ if args.json:
95
+ print(json.dumps(report, ensure_ascii=False, indent=2))
96
+ else:
97
+ for err in errors:
98
+ print(err, file=sys.stderr)
58
99
 
59
100
  return 1 if any(e.startswith("FAIL") for e in errors) else 0
60
101