codeer-cli 0.1.0__tar.gz → 0.1.1__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.
Files changed (29) hide show
  1. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/.gitignore +1 -0
  2. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/API_REFERENCE.md +32 -0
  3. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/PKG-INFO +43 -2
  4. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/README.md +41 -0
  5. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/pyproject.toml +7 -2
  6. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/cli.py +36 -2
  7. codeer_cli-0.1.1/src/codeer_cli/commands/_util.py +66 -0
  8. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/commands/agent.py +117 -10
  9. codeer_cli-0.1.1/src/codeer_cli/commands/check.py +107 -0
  10. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/commands/eval_cmd.py +232 -28
  11. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/commands/history.py +117 -12
  12. codeer_cli-0.1.1/src/codeer_cli/commands/kb.py +350 -0
  13. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/kb.py +52 -0
  14. codeer_cli-0.1.1/uv.lock +124 -0
  15. codeer_cli-0.1.0/src/codeer_cli/commands/_util.py +0 -12
  16. codeer_cli-0.1.0/src/codeer_cli/commands/check.py +0 -66
  17. codeer_cli-0.1.0/src/codeer_cli/commands/kb.py +0 -126
  18. codeer_cli-0.1.0/uv.lock +0 -91
  19. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/__init__.py +0 -0
  20. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/_validate.py +0 -0
  21. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/agents.py +0 -0
  22. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/chats.py +0 -0
  23. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/client.py +0 -0
  24. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/commands/__init__.py +0 -0
  25. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/commands/profile.py +0 -0
  26. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/constants.py +0 -0
  27. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/eval_.py +0 -0
  28. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/histories.py +0 -0
  29. {codeer_cli-0.1.0 → codeer_cli-0.1.1}/src/codeer_cli/parse.py +0 -0
@@ -22,6 +22,7 @@ venv/
22
22
  env/
23
23
  .env
24
24
  .env.*
25
+ *.env
25
26
  !.env.example
26
27
  !.env.sample
27
28
  session.env
@@ -66,6 +66,38 @@ Base path: `/organizations/{org_id}/workspaces/{ws_id}/knowledge_bases`
66
66
  Attach KB files to an agent by listing their node IDs in the agent's
67
67
  `unified_tools[].knowledge_node_ids`.
68
68
 
69
+ ### Context Object FAQ
70
+
71
+ Base path: `/external/context-object-faqs`
72
+
73
+ | Method & path | Purpose |
74
+ | --- | --- |
75
+ | `GET /context-object-faqs` | List FAQ entries, optionally filtered by `context_object_id` |
76
+ | `GET /context-object-faqs/{faq_id}` | Read one FAQ entry |
77
+ | `POST /context-object-faqs` | Create an FAQ entry |
78
+ | `PATCH /context-object-faqs/{faq_id}` | Update the linked context object and/or question |
79
+ | `DELETE /context-object-faqs/{faq_id}` | Delete an FAQ entry |
80
+
81
+ Create body:
82
+
83
+ ```json
84
+ {"context_object_id": 123, "question": "How do I reset billing?"}
85
+ ```
86
+
87
+ Update body accepts either or both fields:
88
+
89
+ ```json
90
+ {"context_object_id": 456, "question": "How do I update billing?"}
91
+ ```
92
+
93
+ `context_object_id` is the KB file's `snapshot_object_id` from the KB node
94
+ listing. The compact CLI output includes it:
95
+
96
+ ```bash
97
+ codeer kb files --kb-id <kb-id>
98
+ codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --dry-run
99
+ ```
100
+
69
101
  ## Stage 3 — Live Test on a specific version
70
102
 
71
103
  | Method & path | Purpose |
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeer-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Command line tools for managing Codeer agents over the Codeer API.
5
5
  Project-URL: Homepage, https://www.codeer.ai
6
6
  Author: Codeer.AI
7
- License: Proprietary
7
+ License: MIT
8
8
  Classifier: Development Status :: 3 - Alpha
9
9
  Classifier: Environment :: Console
10
10
  Classifier: Intended Audience :: Developers
@@ -106,3 +106,44 @@ Validate setup before API work:
106
106
  ```bash
107
107
  codeer check
108
108
  ```
109
+
110
+ ## Output policy for coding agents
111
+
112
+ The CLI is optimized for Codex, Claude Code, Claude Cowork, and similar coding
113
+ agents that keep command output in their LLM context. Default stdout is a
114
+ compact lifecycle summary, not the full server payload.
115
+
116
+ Use this pattern during agent lifecycle work:
117
+
118
+ ```bash
119
+ codeer agent list
120
+ codeer history list --agent <agent-id> --limit 50
121
+ codeer eval run --agent <agent-id> --evaluators <evaluator-id> --out .codeer/eval_run.json
122
+ ```
123
+
124
+ Flags:
125
+
126
+ - `--full` prints bounded extra detail for human inspection. It is still
127
+ intended to be safe for LLM context.
128
+ - `--out <path>` writes complete diagnostic artifacts to a local file. Use it
129
+ for raw eval results, full conversation turns, full rubric matrices, and
130
+ other data that can grow with cases, versions, or turns.
131
+
132
+ Avoid piping large raw JSON directly into agent chat. Prefer `--out`, then ask
133
+ the coding agent to inspect targeted summaries, IDs, failing cases, or selected
134
+ snippets from the saved file.
135
+
136
+ ## Context Object FAQ
137
+
138
+ Use Context Object FAQ entries to route high-value questions to a canonical KB
139
+ file when semantic retrieval misses the right source. The FAQ target is a KB
140
+ file's `snapshot_object_id`, shown by `codeer kb files`.
141
+
142
+ ```bash
143
+ codeer kb files --kb-id <kb-id>
144
+ codeer kb faq-list --context-object-id <snapshot-object-id>
145
+ codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --dry-run
146
+ ```
147
+
148
+ After reviewing the dry-run output, rerun the create/update/delete command
149
+ without `--dry-run` to apply it.
@@ -88,3 +88,44 @@ Validate setup before API work:
88
88
  ```bash
89
89
  codeer check
90
90
  ```
91
+
92
+ ## Output policy for coding agents
93
+
94
+ The CLI is optimized for Codex, Claude Code, Claude Cowork, and similar coding
95
+ agents that keep command output in their LLM context. Default stdout is a
96
+ compact lifecycle summary, not the full server payload.
97
+
98
+ Use this pattern during agent lifecycle work:
99
+
100
+ ```bash
101
+ codeer agent list
102
+ codeer history list --agent <agent-id> --limit 50
103
+ codeer eval run --agent <agent-id> --evaluators <evaluator-id> --out .codeer/eval_run.json
104
+ ```
105
+
106
+ Flags:
107
+
108
+ - `--full` prints bounded extra detail for human inspection. It is still
109
+ intended to be safe for LLM context.
110
+ - `--out <path>` writes complete diagnostic artifacts to a local file. Use it
111
+ for raw eval results, full conversation turns, full rubric matrices, and
112
+ other data that can grow with cases, versions, or turns.
113
+
114
+ Avoid piping large raw JSON directly into agent chat. Prefer `--out`, then ask
115
+ the coding agent to inspect targeted summaries, IDs, failing cases, or selected
116
+ snippets from the saved file.
117
+
118
+ ## Context Object FAQ
119
+
120
+ Use Context Object FAQ entries to route high-value questions to a canonical KB
121
+ file when semantic retrieval misses the right source. The FAQ target is a KB
122
+ file's `snapshot_object_id`, shown by `codeer kb files`.
123
+
124
+ ```bash
125
+ codeer kb files --kb-id <kb-id>
126
+ codeer kb faq-list --context-object-id <snapshot-object-id>
127
+ codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --dry-run
128
+ ```
129
+
130
+ After reviewing the dry-run output, rerun the create/update/delete command
131
+ without `--dry-run` to apply it.
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codeer-cli"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Command line tools for managing Codeer agents over the Codeer API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
  authors = [{ name = "Codeer.AI" }]
12
- license = { text = "Proprietary" }
12
+ license = { text = "MIT" }
13
13
  classifiers = [
14
14
  "Development Status :: 3 - Alpha",
15
15
  "Environment :: Console",
@@ -23,6 +23,11 @@ dependencies = [
23
23
  "httpx>=0.27",
24
24
  ]
25
25
 
26
+ [dependency-groups]
27
+ dev = [
28
+ "ruff>=0.11",
29
+ ]
30
+
26
31
  [project.urls]
27
32
  Homepage = "https://www.codeer.ai"
28
33
 
@@ -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
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def log(*args, **kwargs):
10
+ print(*args, file=sys.stderr, **kwargs)
11
+
12
+
13
+ def truncate(text: str, n: int = 60) -> str:
14
+ text = text.replace("\n", " ").strip()
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
 
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+
6
+ from ..client import AuthError, CodeerError
7
+
8
+
9
+ def register(subparsers):
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")
12
+ p.set_defaults(func=run)
13
+
14
+
15
+ def run(args, client) -> int:
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
+ }
24
+
25
+ try:
26
+ me = client.get_me()
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
34
+ print("FAIL Auth: API key missing, invalid, expired, or revoked (401/403)", file=sys.stderr)
35
+ print(" Create an admin workspace API key and export CODEER_API_KEY before running codeer", file=sys.stderr)
36
+ return 1
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
43
+ print(f"FAIL Auth: {e}", file=sys.stderr)
44
+ return 1
45
+
46
+ profile = me.get("profile", {})
47
+ email = me.get("email") or "(unknown)"
48
+ report["auth"] = {"ok": True, "email": email}
49
+ if not args.json:
50
+ print(f" OK Auth: logged in as {email}")
51
+
52
+ try:
53
+ ws_id, org_id = client.resolve_scope()
54
+ except CodeerError as e:
55
+ errors.append(f"FAIL Scope: {e.message if hasattr(e, 'message') else str(e)}")
56
+ ws_id, org_id = None, None
57
+
58
+ if ws_id:
59
+ ws_name = _workspace_name(profile, ws_id)
60
+ report["workspace"] = {"ok": True, "id": ws_id, "name": ws_name}
61
+ if ws_name:
62
+ if not args.json:
63
+ print(f" OK Workspace: {ws_name} ({ws_id})")
64
+ else:
65
+ if not args.json:
66
+ print(f" OK Workspace: {ws_id}")
67
+
68
+ if org_id:
69
+ report["organization"] = {"ok": True, "id": org_id}
70
+ if not args.json:
71
+ print(f" OK Organization: {org_id}")
72
+
73
+ agent_id = client.agent_id
74
+ if agent_id:
75
+ report["agent"] = {"ok": False, "configured": True, "optional": True, "id": agent_id}
76
+ try:
77
+ agent = client.get(f"/external/agents/{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})")
81
+ except CodeerError:
82
+ errors.append(f"WARN Agent: ID {agent_id} could not be read (may be in a different workspace)")
83
+ else:
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)
99
+
100
+ return 1 if any(e.startswith("FAIL") for e in errors) else 0
101
+
102
+
103
+ def _workspace_name(profile: dict, workspace_id: str) -> str | None:
104
+ for ws in profile.get("workspaces", []) or []:
105
+ if str(ws.get("id")) == str(workspace_id):
106
+ return ws.get("name")
107
+ return None