codeer-cli 0.1.0__py3-none-any.whl → 0.1.2__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/agents.py CHANGED
@@ -153,3 +153,8 @@ def get_version(client: CodeerClient, agent_id: str, history_id: str) -> dict:
153
153
  def check_impact(client: CodeerClient, agent_id: str) -> dict:
154
154
  """List downstream agents that call this one. Call before publishing breaking changes."""
155
155
  return client.get(f"/external/agents/{agent_id}/impact")
156
+
157
+
158
+ def publish_version(client: CodeerClient, agent_id: str, history_id: str) -> dict:
159
+ """Promote one AgentHistory version to the published runtime version."""
160
+ return client.post(f"/external/agents/{agent_id}/versions/{history_id}:publish", json={})
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
codeer_cli/client.py CHANGED
@@ -85,8 +85,7 @@ class CodeerClient:
85
85
  if not api_key:
86
86
  raise AuthError(
87
87
  0,
88
- "Missing API key. Export CODEER_API_KEY or run `codeer profile add <name>` "
89
- "and `codeer profile use <name>`.",
88
+ "Missing API key. Export CODEER_API_KEY or run `codeer profile add <name>`.",
90
89
  )
91
90
 
92
91
  overrides.pop("workspace_id", None)
@@ -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,119 @@ 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
 
72
+ p = sub.add_parser("impact", help="Check downstream agents affected by this agent")
73
+ p.add_argument("--agent", required=True)
74
+ p.add_argument("--out", default=None, help="Write full impact detail to this file too")
75
+ p.set_defaults(func=run_impact)
76
+
77
+ p = sub.add_parser("publish", help="Publish an agent version; run --dry-run first")
78
+ p.add_argument("--agent", required=True)
79
+ g = p.add_mutually_exclusive_group(required=True)
80
+ g.add_argument("--history", default=None, help="AgentHistory UUID to publish")
81
+ g.add_argument("--version", type=int, default=None, help="AgentHistory version_number to publish")
82
+ p.add_argument("--dry-run", action="store_true",
83
+ help="Resolve target version and print intended mutation without writing server state.")
84
+ p.add_argument("--out", default=None, help="Write result JSON to this file too")
85
+ p.set_defaults(func=run_publish)
86
+
87
+
88
+ def _tool_summary(tools: list[dict] | None) -> list[dict]:
89
+ out = []
90
+ for t in tools or []:
91
+ form = t.get("custom_form_schema") if isinstance(t.get("custom_form_schema"), dict) else {}
92
+ out.append({
93
+ "id": t.get("id"),
94
+ "type": t.get("type"),
95
+ "name": t.get("name"),
96
+ "knowledge_node_count": len(t.get("knowledge_node_ids") or []),
97
+ "form_title": form.get("title"),
98
+ "invocation_preview": truncate(t.get("invocation_instruction") or "", 160),
99
+ })
100
+ return out
101
+
102
+
103
+ def _agent_summary(agent: dict, *, full: bool = False) -> dict:
104
+ tools = agent.get("unified_tools") or agent.get("tools") or []
105
+ row = {
106
+ "id": agent.get("id"),
107
+ "name": agent.get("name"),
108
+ "workspace": {
109
+ "id": agent.get("workspace_id") or (agent.get("workspace") or {}).get("id"),
110
+ "name": (agent.get("workspace") or {}).get("name"),
111
+ },
112
+ "publish_state": agent.get("publish_state"),
113
+ "version": agent.get("version"),
114
+ "latest_version_number": agent.get("latest_version_number"),
115
+ "published_version_number": agent.get("published_version_number"),
116
+ "publish_history_id": agent.get("publish_history_id"),
117
+ "llm_model": agent.get("llm_model"),
118
+ "agent_type": agent.get("agent_type"),
119
+ "updated_at": agent.get("updated_at"),
120
+ "tool_count": len(tools),
121
+ "system_prompt_chars": len(agent.get("system_prompt") or ""),
122
+ }
123
+ if full:
124
+ row["description"] = agent.get("description") or ""
125
+ row["use_search"] = agent.get("use_search")
126
+ row["suggested_questions"] = agent.get("suggested_questions") or []
127
+ row["tools"] = _tool_summary(tools)
128
+ row["system_prompt_preview"] = truncate(agent.get("system_prompt") or "", 1200)
129
+ return row
130
+
50
131
 
51
132
 
52
133
  def run_list(args, client) -> int:
53
134
  ws, org = client.resolve_scope()
54
135
  result = agents_mod.list_all(client, workspace_id=ws, organization_id=org)
55
- print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
136
+ write_json(args.out, strip_noisy_fields(result))
137
+ print_json([_agent_summary(a, full=args.full) for a in result])
56
138
  return 0
57
139
 
58
140
 
59
141
  def run_get(args, client) -> int:
60
142
  result = agents_mod.get(client, args.agent_id)
61
- print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
143
+ full_result = strip_noisy_fields(result)
144
+ write_json(args.out, full_result)
145
+ print_json(full_result if args.full else _agent_summary(result))
62
146
  return 0
63
147
 
64
148
 
65
149
  def run_apply(args, client) -> int:
66
150
  body = json.loads(Path(args.payload).read_text())
151
+ missing = [field for field in ("name", "system_prompt") if not body.get(field)]
152
+ if missing:
153
+ log(f"error: payload missing required field(s): {', '.join(missing)}")
154
+ return 2
155
+
156
+ if args.dry_run:
157
+ operation = "update" if args.agent_id else "create"
158
+ result = {
159
+ "dry_run": True,
160
+ "operation": operation,
161
+ "agent_id": args.agent_id,
162
+ "payload": str(Path(args.payload)),
163
+ "name": body.get("name"),
164
+ "system_prompt_chars": len(body.get("system_prompt") or ""),
165
+ "tool_count": len(body.get("unified_tools") or []),
166
+ "use_search": body.get("use_search", False),
167
+ "llm_model": body.get("llm_model"),
168
+ "version_note": args.note if args.agent_id else None,
169
+ "would_write_server_state": True,
170
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
171
+ }
172
+ print_json(result)
173
+ return 0
67
174
 
68
175
  if args.agent_id:
69
176
  body.pop("workspace_id", None)
@@ -118,7 +225,78 @@ def run_apply(args, client) -> int:
118
225
 
119
226
  def run_versions(args, client) -> int:
120
227
  versions = agents_mod.list_versions(client, args.agent)
121
- print(json.dumps(versions, ensure_ascii=False, indent=2, default=str))
228
+ write_json(args.out, strip_noisy_fields(versions))
229
+ rows = []
230
+ for v in versions:
231
+ row = {
232
+ "id": v.get("id"),
233
+ "version_number": v.get("version_number"),
234
+ "status": v.get("status"),
235
+ "was_published": v.get("was_published"),
236
+ "version_note": v.get("version_note") or "",
237
+ "created_at": v.get("created_at"),
238
+ }
239
+ if args.full:
240
+ row["system_prompt_chars"] = len(v.get("system_prompt") or "")
241
+ row["tool_count"] = len(v.get("unified_tools") or v.get("tools") or [])
242
+ rows.append(row)
243
+ print_json(rows)
244
+ return 0
245
+
246
+
247
+ def run_impact(args, client) -> int:
248
+ result = strip_noisy_fields(agents_mod.check_impact(client, args.agent))
249
+ write_json(args.out, result)
250
+ print_json(result)
251
+ return 0
252
+
253
+
254
+ def _resolve_history_for_publish(
255
+ client: CodeerClient,
256
+ agent_id: str,
257
+ history_id: Optional[str],
258
+ version: Optional[int],
259
+ ) -> dict:
260
+ if history_id:
261
+ return agents_mod.get_version(client, agent_id, history_id)
262
+ if version is not None:
263
+ for candidate in agents_mod.list_versions(client, agent_id):
264
+ if candidate.get("version_number") == version:
265
+ return agents_mod.get_version(client, agent_id, candidate["id"])
266
+ raise SystemExit(f"no version {version} on agent {agent_id}")
267
+ raise SystemExit("must pass --history or --version")
268
+
269
+
270
+ def run_publish(args, client) -> int:
271
+ history = _resolve_history_for_publish(client, args.agent, args.history, args.version)
272
+ history_id = history["id"]
273
+ summary = {
274
+ "agent_id": args.agent,
275
+ "history_id": history_id,
276
+ "version_number": history.get("version_number"),
277
+ "status": history.get("status"),
278
+ "was_published": history.get("was_published"),
279
+ "version_note": history.get("version_note") or "",
280
+ }
281
+
282
+ if args.dry_run:
283
+ result = {
284
+ "dry_run": True,
285
+ "operation": "agent_publish",
286
+ "method": "POST",
287
+ "path": f"/external/agents/{args.agent}/versions/{history_id}:publish",
288
+ "target": summary,
289
+ "would_write_server_state": True,
290
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
291
+ }
292
+ print_json(result)
293
+ write_json(args.out, result)
294
+ return 0
295
+
296
+ result = strip_noisy_fields(agents_mod.publish_version(client, args.agent, history_id))
297
+ output = {"target": summary, "response": result}
298
+ print_json(output)
299
+ write_json(args.out, output)
122
300
  return 0
123
301
 
124
302
 
@@ -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