codeer-cli 0.1.15__py3-none-any.whl → 0.1.16__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.
@@ -0,0 +1,109 @@
1
+ """HTTP input *configuration* checks; value conversion belongs to the server.
2
+
3
+ Key generation follows collect_http_request_agent_placeholders in the backend's
4
+ http_request/execution.py. Keep its traversal and collision rules in sync.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import Any
11
+
12
+
13
+ _AGENT_PLACEHOLDER_RE = re.compile(r"\{\{\s*agent\[(.*?)\]\s*\}\}")
14
+ _CONTRACT_FIELDS = {
15
+ "type": ("string", "number", "integer", "boolean", "object", "array"),
16
+ "format": ("text", "json"),
17
+ "json_type": ("any", "object", "array"),
18
+ }
19
+
20
+
21
+ def _contract_defaults(contract: Any, key: str) -> dict[str, str]:
22
+ prefix = f"input_contracts[{key!r}]"
23
+ if not isinstance(contract, dict):
24
+ raise ValueError(f"{prefix} must be an object.")
25
+ if any(field not in _CONTRACT_FIELDS for field in contract):
26
+ raise ValueError(
27
+ f"{prefix} accepts only type, format, json_type (snake_case, not jsonType)."
28
+ )
29
+ resolved = {}
30
+ for field, choices in _CONTRACT_FIELDS.items():
31
+ value = contract.get(field, choices[0])
32
+ if not isinstance(value, str) or value not in choices:
33
+ raise ValueError(f"{prefix}.{field} must be one of: {', '.join(choices)}.")
34
+ resolved[field] = value
35
+ if resolved["format"] == "json" and resolved["type"] != "string":
36
+ raise ValueError(f"{prefix}: format=json is only available for type=string.")
37
+ if resolved["format"] != "json" and resolved["json_type"] != "any":
38
+ raise ValueError(f"{prefix}: json_type requires format=json unless it is any.")
39
+ return resolved
40
+
41
+
42
+ def validate_http_input_contracts(body: Any) -> list[dict[str, Any]]:
43
+ """Validate without mutation and return value-free input format summaries.
44
+
45
+ An omitted body is supplied as {} by callers; template may be null/absent.
46
+ Unconfigured placeholders retain string/text/any, including mixed text.
47
+ Unknown body fields follow the backend's existing ignore policy, except the
48
+ frontend alias inputContracts: reject it rather than silently losing intent.
49
+ Contract entries themselves are strict and reject all unknown fields.
50
+ """
51
+ if not isinstance(body, dict):
52
+ raise ValueError("body must be an object; omit it for no body.")
53
+ if "inputContracts" in body:
54
+ raise ValueError("use input_contracts (snake_case), not inputContracts.")
55
+ contracts = body.get("input_contracts", {})
56
+ if not isinstance(contracts, dict) or any(
57
+ not isinstance(key, str) for key in contracts
58
+ ):
59
+ raise ValueError("input_contracts must be an object with string keys.")
60
+ resolved = {
61
+ key: _contract_defaults(contract, key) for key, contract in contracts.items()
62
+ }
63
+ summaries: list[dict[str, Any]] = []
64
+ used_keys: set[str] = set()
65
+
66
+ def walk(value: Any, path: str) -> None:
67
+ if isinstance(value, dict):
68
+ for key, item in value.items():
69
+ walk(item, f"{path}.{key}" if path else key)
70
+ return
71
+ if isinstance(value, list):
72
+ for index, item in enumerate(value):
73
+ walk(item, f"{path}[{index}]")
74
+ return
75
+ if not isinstance(value, str):
76
+ return
77
+
78
+ matches = list(_AGENT_PLACEHOLDER_RE.finditer(value))
79
+ for index, match in enumerate(matches):
80
+ if not match.group(1).strip():
81
+ raise ValueError("Agent placeholder instruction cannot be empty.")
82
+ base = (
83
+ re.sub(r"[^a-zA-Z0-9]+", "_", path or "body").strip("_").lower()
84
+ or "body"
85
+ )
86
+ if len(matches) > 1:
87
+ base = f"{base}_{index + 1}"
88
+ key = base
89
+ suffix = 2
90
+ while key in used_keys:
91
+ key = f"{base}_{suffix}"
92
+ suffix += 1
93
+ used_keys.add(key)
94
+ contract = resolved.get(key) or _contract_defaults({}, key)
95
+ if (contract["type"] != "string" or contract["format"] == "json") and (
96
+ len(matches) != 1 or match.group(0) != value
97
+ ):
98
+ raise ValueError(
99
+ f"Input {key!r}: typed and JSON-text inputs must occupy the entire template value."
100
+ )
101
+ summaries.append({"key": key, **contract, "configured": key in contracts})
102
+
103
+ walk(body.get("template"), "")
104
+ if set(contracts) - used_keys:
105
+ raise ValueError(
106
+ "input_contracts contains keys that do not match current placeholders; "
107
+ "remove stale contracts or regenerate keys after changing template paths/order."
108
+ )
109
+ return summaries
codeer_cli/_validate.py CHANGED
@@ -3,13 +3,15 @@
3
3
  These checks exist because the backend's form-schema validator is lenient
4
4
  (``extra="allow"``) and silently accepts unknown ``type`` strings, which then
5
5
  render as blank fields in the web builder. Catching the common mistakes here
6
- gives actionable errors before the PUT/POST round-trip.
6
+ gives actionable errors before the PATCH/POST round-trip. HTTP input contracts
7
+ also need strict configuration checks before a draft is saved.
7
8
  """
8
9
 
9
10
  from __future__ import annotations
10
11
 
11
12
  from typing import Any, Iterable
12
13
 
14
+ from ._http_contracts import validate_http_input_contracts
13
15
  from .constants import (
14
16
  FORM_FIELD_TYPES,
15
17
  MAX_CALL_AGENT_TOOLS,
@@ -121,10 +123,14 @@ def _validate_single_tool(tool: dict[str, Any], index: int) -> None:
121
123
  raise ToolValidationError(f"{prefix}: call_agent tool requires agent_id.")
122
124
  elif tool_type == "http_request":
123
125
  cfg = tool.get("http_request")
124
- if not cfg or not cfg.get("method") or not cfg.get("url_template"):
126
+ if not isinstance(cfg, dict) or not cfg.get("method") or not cfg.get("url_template"):
125
127
  raise ToolValidationError(
126
128
  f"{prefix}: http_request tool requires http_request.method and http_request.url_template."
127
129
  )
130
+ try:
131
+ validate_http_input_contracts(cfg.get("body", {}))
132
+ except ValueError as exc:
133
+ raise ToolValidationError(f"{prefix}.http_request.body: {exc}") from None
128
134
 
129
135
 
130
136
  def _validate_form_schema(schema: Any, prefix: str) -> None:
codeer_cli/agents.py CHANGED
@@ -66,7 +66,7 @@ def update(
66
66
  attachment_ids: Optional[List[str]] = None,
67
67
  human_handoff: Optional[dict[str, Any]] = None,
68
68
  ) -> dict:
69
- """PUT creates a new AgentHistory snapshot (draft)."""
69
+ """PATCH replaces settings and creates a draft; unified_tools is the full list."""
70
70
  validated_tools = validate_unified_tools(unified_tools)
71
71
  validated_handoff = validate_human_handoff(human_handoff)
72
72
  body: dict[str, Any] = {
codeer_cli/cli.py CHANGED
@@ -5,7 +5,7 @@
5
5
  codeer model list
6
6
  codeer kb list|files|export|upload|node-rename|node-delete|faq-list|faq-get|faq-create|faq-update|faq-delete
7
7
  codeer eval list|label-list|label-create|label-update|label-delete|case-update|case-delete|evaluators|evaluator-create|evaluator-update|run|export|reconcile|cases-apply|rubrics|rubrics-apply
8
- codeer history list|get|conversations|negative-feedback|create|send
8
+ codeer history list|get|conversations|ai-drafts|negative-feedback|create|send
9
9
  """
10
10
 
11
11
  from __future__ import annotations
@@ -16,6 +16,7 @@ import sys
16
16
 
17
17
  from .client import AuthError, CodeerClient, CodeerError
18
18
  from .commands import check
19
+ from .histories import HistoryExportError
19
20
 
20
21
 
21
22
  def main(argv: list[str] | None = None) -> int:
@@ -127,6 +128,9 @@ Use --out <path> for large raw artifacts; stdout defaults to compact summaries.
127
128
 
128
129
  try:
129
130
  return args.func(args, client)
131
+ except HistoryExportError as e:
132
+ print(f"error: {e}", file=sys.stderr)
133
+ return 1
130
134
  except AuthError as e:
131
135
  print(f"auth: {e}", file=sys.stderr)
132
136
  return 3
codeer_cli/client.py CHANGED
@@ -161,6 +161,7 @@ class CodeerClient:
161
161
  files: Any = None,
162
162
  data: Any = None,
163
163
  timeout: Optional[float] = None,
164
+ unwrap: bool = True,
164
165
  ) -> Any:
165
166
  url = _api_url(path, api_version=api_version)
166
167
  request_kwargs: dict[str, Any] = {}
@@ -186,7 +187,7 @@ class CodeerClient:
186
187
  ) from exc
187
188
  except httpx.RequestError as exc:
188
189
  raise self._transport_error(method_upper, path, exc) from exc
189
- return self._parse(r)
190
+ return self._parse(r, unwrap=unwrap)
190
191
 
191
192
  def get(self, path: str, **kwargs: Any) -> Any:
192
193
  return self.request("GET", path, **kwargs)
@@ -290,7 +291,7 @@ class CodeerClient:
290
291
  },
291
292
  )
292
293
 
293
- def _parse(self, r: httpx.Response) -> Any:
294
+ def _parse(self, r: httpx.Response, *, unwrap: bool = True) -> Any:
294
295
  text = r.text
295
296
  try:
296
297
  payload = r.json() if text else None
@@ -306,7 +307,7 @@ class CodeerClient:
306
307
  if isinstance(payload, dict) and "error_code" in payload and "data" in payload:
307
308
  if payload.get("error_code") not in (0, None):
308
309
  raise CodeerError(r.status_code, payload.get("message") or "error", payload)
309
- return payload["data"]
310
+ return payload["data"] if unwrap else payload
310
311
  return payload
311
312
 
312
313
  def _raise_for_error(self, status: int, payload: Any) -> None:
@@ -34,7 +34,12 @@ NOISY_KEYS = {
34
34
 
35
35
 
36
36
  def strip_noisy_fields(value: Any) -> Any:
37
- """Remove server/account metadata that is not useful for agent lifecycle work."""
37
+ """Simplify metadata on one API resource (or a list of resources).
38
+
39
+ Callers pass unwrapped resource records, not response envelopes. Nested
40
+ dictionaries are opaque content: templates, schemas, tool results and other
41
+ user data can legally contain any of the metadata names. Do not recurse.
42
+ """
38
43
  if isinstance(value, list):
39
44
  return [strip_noisy_fields(item) for item in value]
40
45
  if not isinstance(value, dict):
@@ -51,7 +56,7 @@ def strip_noisy_fields(value: Any) -> Any:
51
56
  if item.get(k) is not None
52
57
  }
53
58
  continue
54
- out[key] = strip_noisy_fields(item)
59
+ out[key] = item
55
60
  return out
56
61
 
57
62
 
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from typing import Optional
7
7
 
8
8
  from .. import agents as agents_mod
9
+ from .._http_contracts import validate_http_input_contracts
9
10
  from .._validate import validate_human_handoff, validate_unified_tools
10
11
  from ..client import CodeerClient
11
12
  from ._util import log, print_json, strip_noisy_fields, truncate, write_json
@@ -32,6 +33,7 @@ def register(subparsers):
32
33
  help="Read one agent. Defaults to summary; use --full for prompt/tool detail or --out for an artifact.",
33
34
  )
34
35
  p.add_argument("agent_id")
36
+ p.add_argument("--history", default=None, help="Read this exact AgentHistory UUID instead of the editable agent.")
35
37
  p.add_argument("--full", action="store_true",
36
38
  help="Print stripped full agent config, including system_prompt and tools.")
37
39
  p.add_argument("--out", default=None,
@@ -41,8 +43,8 @@ def register(subparsers):
41
43
  # codeer agent apply --payload agent.json
42
44
  p = sub.add_parser("apply", help="Create or update agent from JSON payload; run --dry-run first")
43
45
  p.add_argument("--payload", required=True, help="Path to agent payload JSON")
44
- p.add_argument("--agent-id", default=None, help="If set, PUT (update). Else POST (create).")
45
- p.add_argument("--note", default="", help="version_note for PUT")
46
+ p.add_argument("--agent-id", default=None, help="If set, PATCH (update). Else POST (create).")
47
+ p.add_argument("--note", default="", help="version_note for PATCH")
46
48
  p.add_argument("--dry-run", action="store_true",
47
49
  help="Validate payload and print intended mutation without writing server state.")
48
50
  p.add_argument("--out", default=None, help="Write result JSON to this file too")
@@ -61,13 +63,13 @@ def register(subparsers):
61
63
  # codeer agent versions --agent <id>
62
64
  p = sub.add_parser(
63
65
  "versions",
64
- help="List version history. Defaults to version metadata only; use --out for full snapshots.",
66
+ help="List version metadata; use agent get --history <id> for a stored snapshot.",
65
67
  )
66
68
  p.add_argument("--agent", required=True)
67
69
  p.add_argument("--full", action="store_true",
68
- help="Add bounded prompt/tool size metadata; full snapshots still require --out.")
70
+ help="Add prompt/tool size metadata when included by the server.")
69
71
  p.add_argument("--out", default=None,
70
- help="Write stripped full version snapshots to this file; stdout stays compact.")
72
+ help="Write the full version listing (metadata, not snapshots); stdout stays compact.")
71
73
  p.set_defaults(func=run_versions)
72
74
 
73
75
  p = sub.add_parser("impact", help="Check downstream agents affected by this agent")
@@ -130,6 +132,8 @@ def _agent_summary(agent: dict, *, full: bool = False) -> dict:
130
132
  "human_handoff_enabled": bool(human_handoff.get("enabled")),
131
133
  "system_prompt_chars": len(agent.get("system_prompt") or ""),
132
134
  }
135
+ if "version_number" in agent:
136
+ row.update({key: agent.get(key) for key in ("agent_id", "version_number", "status")})
133
137
  if full:
134
138
  row["description"] = agent.get("description") or ""
135
139
  row["use_search"] = agent.get("use_search")
@@ -150,7 +154,11 @@ def run_list(args, client) -> int:
150
154
 
151
155
 
152
156
  def run_get(args, client) -> int:
153
- result = agents_mod.get(client, args.agent_id)
157
+ history_id = getattr(args, "history", None)
158
+ result = (
159
+ agents_mod.get_version(client, args.agent_id, history_id)
160
+ if history_id else agents_mod.get(client, args.agent_id)
161
+ )
154
162
  full_result = strip_noisy_fields(result)
155
163
  write_json(args.out, full_result)
156
164
  print_json(full_result if args.full else _agent_summary(result))
@@ -183,6 +191,15 @@ def run_apply(args, client) -> int:
183
191
  "name": body.get("name"),
184
192
  "system_prompt_chars": len(body.get("system_prompt") or ""),
185
193
  "tool_count": len(validated_tools),
194
+ "http_inputs": [
195
+ {
196
+ "tool_index": index,
197
+ "body_inputs_used": tool["http_request"]["method"] not in ("GET", "HEAD"),
198
+ "inputs": validate_http_input_contracts(tool["http_request"].get("body", {})),
199
+ }
200
+ for index, tool in enumerate(validated_tools)
201
+ if tool.get("type") == "http_request"
202
+ ],
186
203
  "use_search": body.get("use_search", False),
187
204
  "llm_model": body.get("llm_model"),
188
205
  "llm_model_settings_provided": llm_model_settings_provided,
@@ -223,7 +240,7 @@ def run_apply(args, client) -> int:
223
240
  **model_settings_kwargs,
224
241
  )
225
242
  agent_id = args.agent_id
226
- log(f"PUT /agents/{agent_id} ok")
243
+ log(f"PATCH /agents/{agent_id} ok")
227
244
  else:
228
245
  if not body.get("workspace_id"):
229
246
  body["workspace_id"] = client.resolve_scope()[0]
@@ -274,8 +291,10 @@ def run_versions(args, client) -> int:
274
291
  "created_at": v.get("created_at"),
275
292
  }
276
293
  if args.full:
277
- row["system_prompt_chars"] = len(v.get("system_prompt") or "")
278
- row["tool_count"] = len(v.get("unified_tools") or v.get("tools") or [])
294
+ if "system_prompt" in v:
295
+ row["system_prompt_chars"] = len(v.get("system_prompt") or "")
296
+ if "unified_tools" in v or "tools" in v:
297
+ row["tool_count"] = len(v.get("unified_tools") or v.get("tools") or [])
279
298
  rows.append(row)
280
299
  print_json(rows)
281
300
  return 0
@@ -371,7 +390,9 @@ def _summarize_tool(t: dict) -> str:
371
390
  "knowledge_node_ids", "domain", "agent_id",
372
391
  "custom_form_schema", "http_request")
373
392
  safe = {k: t.get(k) for k in keep_keys if t.get(k) is not None}
374
- return json.dumps(safe, ensure_ascii=False, indent=2, sort_keys=True)
393
+ # HTTP placeholder keys depend on template insertion order. Sorting would
394
+ # hide a reordering that changes which input receives a collision suffix.
395
+ return json.dumps(safe, ensure_ascii=False, indent=2, sort_keys=t.get("type") != "http_request")
375
396
 
376
397
 
377
398
  def run_diff(args, client) -> int:
@@ -23,6 +23,8 @@ def register(subparsers):
23
23
  p.add_argument("--agent", default=None)
24
24
  p.add_argument("--user", default=None, help="Filter by external_user_id")
25
25
  p.add_argument("--feedback", default=None, help="positive / negative / any")
26
+ p.add_argument("--has-ai-drafts", action="store_true",
27
+ help="Only list histories with at least one AI Draft.")
26
28
  p.add_argument("--exclude-users", default=None,
27
29
  help="Comma-separated external_user_ids to exclude")
28
30
  p.add_argument("--version", type=int, default=None,
@@ -61,6 +63,18 @@ def register(subparsers):
61
63
  help="external_user_id required with --client-visible.")
62
64
  p.set_defaults(func=run_conversations)
63
65
 
66
+ # codeer history ai-drafts <id>
67
+ p = sub.add_parser(
68
+ "ai-drafts",
69
+ help="Export every paginated AI Draft lifecycle record returned for one History.",
70
+ )
71
+ p.add_argument("history_id", type=int)
72
+ p.add_argument("--full", action="store_true",
73
+ help="Require --out and opt into sensitive content previews on stdout.")
74
+ p.add_argument("--out", default=None,
75
+ help="Write every AI Draft record, refinement signal, outcome, tool activity, and delivery.")
76
+ p.set_defaults(func=run_ai_drafts)
77
+
64
78
  # codeer history negative-feedback
65
79
  p = sub.add_parser("negative-feedback", help="Surface assistant turns with negative feedback")
66
80
  p.add_argument("--agent", required=True)
@@ -148,6 +162,15 @@ def _history_summary(row: dict, *, full: bool = False) -> dict:
148
162
  "feedback_counts": _feedback_counts(row),
149
163
  "snippet_preview": truncate(row.get("snippet") or "", 240),
150
164
  }
165
+ for key in (
166
+ "ai_draft_count",
167
+ "dismissed_draft_count",
168
+ "regenerated_draft_count",
169
+ "applied_draft_count",
170
+ "sent_from_ai_draft_count",
171
+ ):
172
+ if key in row:
173
+ out[key] = row.get(key)
151
174
  if full:
152
175
  out["share_type"] = row.get("share_type")
153
176
  meta = row.get("meta") or {}
@@ -190,6 +213,9 @@ def _part_summary(part: dict, idx: int, *, full: bool = False) -> dict:
190
213
  "attachment_count": len(part.get("attached_files") or []),
191
214
  "feedback_count": len(part.get("feedbacks") or []),
192
215
  }
216
+ if is_tool_part:
217
+ # Exact tool payloads belong in the artifact, never terminal previews.
218
+ row.pop("content_preview", None)
193
219
  if is_tool_part and isinstance(raw_content, dict):
194
220
  row.update({
195
221
  "tool_name": raw_content.get("tool_name"),
@@ -224,6 +250,7 @@ def run_list(args, client) -> int:
224
250
  organization_id=organization_id,
225
251
  external_user_id=args.user,
226
252
  feedback_filter=args.feedback,
253
+ has_ai_drafts=True if getattr(args, "has_ai_drafts", False) else None,
227
254
  exclude_users=exclude,
228
255
  limit=args.limit,
229
256
  offset=args.offset,
@@ -301,11 +328,80 @@ def run_conversations(args, client) -> int:
301
328
  "part_summaries_truncated": len(parts) > len(shown_parts),
302
329
  "wrote_full_detail": bool(args.out),
303
330
  "stdout_is_summary": True,
331
+ "read_contract": result.get("export_contract") or "client-visible-chat-v2",
332
+ "provider_raw_trace": result.get("provider_raw_trace", "not_included"),
333
+ "missing_parts_do_not_prove_non_execution": True,
334
+ "omitted_part_summaries": len(parts) - len(shown_parts),
304
335
  "parts": [_part_summary(p, i, full=args.full) for i, p in enumerate(shown_parts)],
305
336
  })
306
337
  return 0
307
338
 
308
339
 
340
+ def _ai_draft_summary(draft: dict, *, full: bool = False) -> dict:
341
+ delivery = draft.get("delivery") or {}
342
+ row = {
343
+ "id": draft.get("id"),
344
+ "history_id": draft.get("history_id"),
345
+ "thread_key": draft.get("thread_key"),
346
+ "context_through_sequence": draft.get("context_through_sequence"),
347
+ "outcome": draft.get("outcome"),
348
+ "result_type": draft.get("result_type"),
349
+ "refinement_source_draft_id": draft.get("refinement_source_draft_id"),
350
+ "has_generation_instruction": bool(draft.get("generation_instruction")),
351
+ "dismiss_reason": draft.get("dismiss_reason"),
352
+ "has_dismiss_feedback": bool(draft.get("dismiss_feedback")),
353
+ "has_generated_content": bool(draft.get("content")),
354
+ "has_actual_content": bool(delivery.get("actual_content")),
355
+ "delivery_status": delivery.get("status"),
356
+ "tool_activity_count": len(draft.get("tool_activities") or []),
357
+ "has_proposed_actions": draft.get("proposed_actions") is not None,
358
+ "created_at": draft.get("created_at"),
359
+ }
360
+ if full:
361
+ row.update({
362
+ "generation_instruction_preview": truncate(draft.get("generation_instruction") or "", 600),
363
+ "dismiss_feedback_preview": truncate(draft.get("dismiss_feedback") or "", 600),
364
+ "content_preview": truncate(draft.get("content") or "", 600),
365
+ "actual_content_preview": truncate(delivery.get("actual_content") or "", 600),
366
+ })
367
+ return row
368
+
369
+
370
+ def run_ai_drafts(args, client) -> int:
371
+ if args.full and not args.out:
372
+ log("error: AI Draft content previews require --out <path>")
373
+ return 2
374
+
375
+ result = hist_mod.list_ai_drafts(client, args.history_id)
376
+ drafts = result["drafts"]
377
+ write_json(args.out, result)
378
+ stdout_limit = 50 if args.full else 20
379
+ shown_drafts = drafts[:stdout_limit]
380
+ outcome_counts: dict[str, int] = {}
381
+ for draft in drafts:
382
+ outcome = str(draft.get("outcome") or "unknown")
383
+ outcome_counts[outcome] = outcome_counts.get(outcome, 0) + 1
384
+ print_json({
385
+ "history_id": args.history_id,
386
+ "draft_count": len(drafts),
387
+ "outcome_counts": outcome_counts,
388
+ "regenerated_draft_count": sum(
389
+ 1 for draft in drafts if draft.get("refinement_source_draft_id") is not None
390
+ ),
391
+ "sent_from_ai_draft_count": sum(
392
+ 1 for draft in drafts if (draft.get("delivery") or {}).get("status") == "sent"
393
+ ),
394
+ "draft_summaries_shown": len(shown_drafts),
395
+ "draft_summaries_truncated": len(drafts) > len(shown_drafts),
396
+ "pages_fetched": result["pages_fetched"],
397
+ "snapshot_consistency": result.get("snapshot_consistency", "best-effort"),
398
+ "wrote_full_detail": bool(args.out),
399
+ "stdout_is_summary": True,
400
+ "drafts": [_ai_draft_summary(draft, full=args.full) for draft in shown_drafts],
401
+ })
402
+ return 0
403
+
404
+
309
405
  def run_negative_feedback(args, client) -> int:
310
406
  exclude = _parse_exclude(args.exclude_users)
311
407
  workspace_id, organization_id = client.resolve_scope()
codeer_cli/histories.py CHANGED
@@ -4,10 +4,12 @@ Use this after an agent has been published and running for a while, to pull
4
4
  recent traffic, filter by feedback, and feed the failing cases back into the
5
5
  evaluation loop.
6
6
 
7
- Pagination: ``/histories`` and the management parts export use ``limit`` +
8
- ``offset`` (NOT ``page`` / ``page_size``). History metadata remains a bounded
9
- caller-selected page. The parts export follows every server page and rejects a
10
- cross-page revision change instead of returning a mixed snapshot.
7
+ Pagination: ``/histories``, AI Draft lifecycle export, and the management parts
8
+ export use ``limit`` + ``offset`` (NOT ``page`` / ``page_size``). History
9
+ metadata remains a bounded caller-selected page. Draft and parts exports follow
10
+ every server page. The parts export additionally rejects a cross-page revision
11
+ change instead of returning a mixed snapshot. The AI Draft endpoint does not
12
+ provide a revision token, so its multi-page export is explicitly best-effort.
11
13
  """
12
14
 
13
15
  from __future__ import annotations
@@ -15,7 +17,11 @@ from __future__ import annotations
15
17
  import builtins
16
18
  from typing import Any, Iterable, Optional
17
19
 
18
- from .client import CodeerClient
20
+ from .client import CodeerClient, CodeerError
21
+
22
+
23
+ class HistoryExportError(ValueError):
24
+ """The server cannot provide a complete, consistent History parts export."""
19
25
 
20
26
 
21
27
  def list(
@@ -26,6 +32,7 @@ def list(
26
32
  organization_id: Optional[str] = None,
27
33
  external_user_id: Optional[str] = None,
28
34
  feedback_filter: Optional[str] = None,
35
+ has_ai_drafts: Optional[bool] = None,
29
36
  exclude_users: Iterable[str] = (),
30
37
  limit: int = 500,
31
38
  offset: int = 0,
@@ -48,6 +55,8 @@ def list(
48
55
  params["external_user_id"] = external_user_id
49
56
  if feedback_filter:
50
57
  params["feedback_filter"] = feedback_filter
58
+ if has_ai_drafts is not None:
59
+ params["has_ai_drafts"] = has_ai_drafts
51
60
  rows = client.get("/external/histories", params=params)
52
61
  drop = {e.lower() for e in exclude_users}
53
62
  if drop:
@@ -55,6 +64,85 @@ def list(
55
64
  return rows
56
65
 
57
66
 
67
+ def list_ai_drafts(client: CodeerClient, history_id: int, *, limit: int = 500) -> dict[str, Any]:
68
+ """Export every AI Draft lifecycle record returned while paging one History.
69
+
70
+ The endpoint returns pagination beside ``data`` in the standard response
71
+ envelope, so this read explicitly preserves the envelope and follows the
72
+ server-reported page size. Draft rows are returned unmodified. Count changes
73
+ and duplicate IDs are rejected, but without a server revision token this is
74
+ not a transactionally consistent point-in-time snapshot.
75
+ """
76
+ if limit <= 0:
77
+ raise ValueError("limit must be greater than zero")
78
+
79
+ offset = 0
80
+ pages_fetched = 0
81
+ total_records: int | None = None
82
+ drafts: list[dict] = []
83
+ seen_ids: set[Any] = set()
84
+
85
+ while True:
86
+ envelope = client.get(
87
+ f"/external/histories/{history_id}/ai-drafts",
88
+ params={"limit": limit, "offset": offset},
89
+ unwrap=False,
90
+ )
91
+ if not isinstance(envelope, dict):
92
+ raise ValueError("AI Draft response must be an envelope object")
93
+ page_drafts = envelope.get("data")
94
+ page_info = envelope.get("pagination")
95
+ if not isinstance(page_drafts, builtins.list):
96
+ raise ValueError("AI Draft response must contain a data list")
97
+ if not isinstance(page_info, dict):
98
+ raise ValueError("AI Draft response must contain pagination metadata")
99
+
100
+ page_offset = page_info.get("offset")
101
+ page_total = page_info.get("total_records")
102
+ page_limit = page_info.get("limit")
103
+ if not all(isinstance(value, int) for value in (page_offset, page_total, page_limit)):
104
+ raise ValueError("AI Draft pagination must contain integer limit/offset/total_records")
105
+ if page_offset != offset:
106
+ raise ValueError(f"AI Draft page offset mismatch: requested {offset}, received {page_offset}")
107
+ if page_total < 0 or page_limit <= 0:
108
+ raise ValueError("AI Draft pagination metadata is invalid")
109
+
110
+ if total_records is None:
111
+ total_records = page_total
112
+ elif page_total != total_records:
113
+ raise ValueError("AI Draft total_records changed while paging; retry the export")
114
+ if len(page_drafts) > page_limit or offset + len(page_drafts) > total_records:
115
+ raise ValueError("AI Draft page contains more rows than its pagination metadata allows")
116
+
117
+ for draft in page_drafts:
118
+ if not isinstance(draft, dict):
119
+ raise ValueError("AI Draft data rows must be objects")
120
+ draft_id = draft.get("id")
121
+ if not isinstance(draft_id, int):
122
+ raise ValueError("AI Draft data rows must contain an integer id")
123
+ if draft_id in seen_ids:
124
+ raise ValueError("AI Draft pagination returned a duplicate draft id")
125
+ seen_ids.add(draft_id)
126
+ drafts.append(draft)
127
+
128
+ pages_fetched += 1
129
+ offset += len(page_drafts)
130
+ if offset >= total_records:
131
+ break
132
+ if not page_drafts:
133
+ raise ValueError("AI Draft pagination stopped before total_records was reached")
134
+
135
+ assert total_records is not None
136
+ return {
137
+ "history_id": history_id,
138
+ "source_endpoint": f"/api/v1/external/histories/{history_id}/ai-drafts",
139
+ "snapshot_consistency": "best-effort",
140
+ "total_records": total_records,
141
+ "pages_fetched": pages_fetched,
142
+ "drafts": drafts,
143
+ }
144
+
145
+
58
146
  def list_negative_feedback_turns(
59
147
  client: CodeerClient,
60
148
  *,
@@ -162,14 +250,14 @@ def list_messages(
162
250
  ) -> dict:
163
251
  """Return every persisted diagnostic part through the management export.
164
252
 
165
- This route is for workspace editors analyzing a History. It is distinct
253
+ This route requires a workspace admin API key to analyze a History. It is distinct
166
254
  from the external client-owner Chat V2 route and must return the explicit
167
255
  ``history-parts-v1`` contract. Tool calls and returns are preserved in the
168
256
  returned artifact. System prompts and provider raw traces are outside the
169
257
  contract.
170
258
  """
171
259
  if limit <= 0:
172
- raise ValueError("limit must be greater than zero")
260
+ raise HistoryExportError("limit must be greater than zero")
173
261
 
174
262
  offset = 0
175
263
  pages_fetched = 0
@@ -179,43 +267,54 @@ def list_messages(
179
267
  messages: list[dict] = []
180
268
 
181
269
  while True:
182
- page = client.get(
183
- f"/external/histories/{history_id}/messages",
184
- params={"limit": limit, "offset": offset},
185
- )
270
+ try:
271
+ page = client.get(
272
+ f"/external/histories/{history_id}/messages",
273
+ params={"limit": limit, "offset": offset},
274
+ )
275
+ except CodeerError as exc:
276
+ if exc.status == 404:
277
+ raise CodeerError(404,
278
+ "History or its parts export endpoint is unavailable. "
279
+ "This command requires a server supporting history-parts-v1; no fallback was attempted.",
280
+ exc.body) from exc
281
+ raise
186
282
  if not isinstance(page, dict):
187
- raise ValueError("History parts response must be an object")
283
+ raise HistoryExportError("History parts response must be an object")
188
284
  if page.get("export_contract") != "history-parts-v1":
189
- raise ValueError("History parts response is missing export_contract=history-parts-v1")
285
+ raise HistoryExportError("History parts response is missing export_contract=history-parts-v1")
190
286
 
191
287
  page_messages = page.get("messages")
192
288
  page_info = page.get("page")
193
289
  revision = page.get("part_revision")
194
290
  if not isinstance(page_messages, builtins.list):
195
- raise ValueError("History parts response must contain a messages list")
291
+ raise HistoryExportError("History parts response must contain a messages list")
196
292
  if not isinstance(page_info, dict):
197
- raise ValueError("History parts response must contain page metadata")
293
+ raise HistoryExportError("History parts response must contain page metadata")
198
294
  if not isinstance(revision, str) or not revision:
199
- raise ValueError("History parts response must contain part_revision")
295
+ raise HistoryExportError("History parts response must contain part_revision")
200
296
 
201
297
  page_offset = page_info.get("offset")
202
298
  page_total = page_info.get("total_records")
203
299
  page_limit = page_info.get("limit")
204
300
  if not all(isinstance(value, int) for value in (page_offset, page_total, page_limit)):
205
- raise ValueError("History parts page metadata must contain integer limit/offset/total_records")
301
+ raise HistoryExportError("History parts page metadata must contain integer limit/offset/total_records")
206
302
  if page_offset != offset:
207
- raise ValueError(f"History parts page offset mismatch: requested {offset}, received {page_offset}")
303
+ raise HistoryExportError(f"History parts page offset mismatch: requested {offset}, received {page_offset}")
208
304
  if page_total < 0 or page_limit <= 0:
209
- raise ValueError("History parts page metadata is invalid")
305
+ raise HistoryExportError("History parts page metadata is invalid")
210
306
 
211
307
  if total_records is None:
212
308
  total_records = page_total
213
309
  elif page_total != total_records:
214
- raise ValueError("History parts total_records changed while paging; retry the export")
310
+ raise HistoryExportError("History parts total_records changed while paging; retry the export")
215
311
  if part_revision is None:
216
312
  part_revision = revision
217
313
  elif revision != part_revision:
218
- raise ValueError("History parts changed while paging; retry the export")
314
+ raise HistoryExportError("History parts changed while paging; retry the export")
315
+
316
+ if len(page_messages) > page_limit or offset + len(page_messages) > total_records:
317
+ raise HistoryExportError("History parts page contains more rows than its pagination metadata allows")
219
318
 
220
319
  if result is None:
221
320
  result = dict(page)
@@ -223,10 +322,10 @@ def list_messages(
223
322
  pages_fetched += 1
224
323
  offset += len(page_messages)
225
324
 
226
- if offset >= total_records:
325
+ if offset == total_records:
227
326
  break
228
- if not page_messages:
229
- raise ValueError("History parts pagination stopped before total_records was reached")
327
+ if len(page_messages) != page_limit:
328
+ raise HistoryExportError("Incomplete History messages page")
230
329
 
231
330
  assert result is not None
232
331
  assert total_records is not None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeer-cli
3
- Version: 0.1.15
3
+ Version: 0.1.16
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
@@ -175,6 +175,104 @@ becomes available in live published-agent conversations with a non-empty
175
175
  `external_user_id`; editor Live Test conversations are internal and cannot
176
176
  activate human mode.
177
177
 
178
+ ## HTTP input contracts
179
+
180
+ `codeer agent apply --payload` and SDK `agents.create` / `agents.update` accept
181
+ `unified_tools[].http_request.body.input_contracts`. No separate HTTP command is
182
+ needed. The target backend must have the HTTP input-contract feature deployed
183
+ (codeer-copilot #1495); installing this CLI alone does not enable runtime support.
184
+ A local dry-run cannot establish server deployment or API business-rule success.
185
+
186
+ Example payload:
187
+
188
+ ```json
189
+ {
190
+ "name": "Order helper",
191
+ "system_prompt": "Use the configured API for approved order changes.",
192
+ "use_search": false,
193
+ "unified_tools": [{
194
+ "id": "submit",
195
+ "type": "http_request",
196
+ "http_request": {
197
+ "method": "POST",
198
+ "url_template": "https://example.com/orders",
199
+ "body": {
200
+ "template": {
201
+ "quantity": "{{agent[Requested quantity]}}",
202
+ "payload": "{{agent[Order details]}}",
203
+ "changes": "{{agent[Changes as JSON text]}}"
204
+ },
205
+ "input_contracts": {
206
+ "quantity": {"type": "integer"},
207
+ "payload": {"type": "object"},
208
+ "changes": {"type": "string", "format": "json", "json_type": "array"}
209
+ }
210
+ }
211
+ }
212
+ }]
213
+ }
214
+ ```
215
+
216
+ - `type`: `string` (default), `number`, `integer`, `boolean`, `object`, `array`.
217
+ - `format`: `text` (default) or `json`; `json` requires `type: string`.
218
+ - `json_type`: `any` (default), `object`, `array`; outside JSON format, only
219
+ `any` is valid. API names are snake_case; the CLI rejects `inputContracts`,
220
+ `jsonType`, and unknown fields inside individual contracts.
221
+
222
+ `type: object` / `array` sends a native JSON value. `type: string, format: json`
223
+ sends a string containing JSON. Existing valid JSON text is sent unchanged;
224
+ empty strings also pass unchanged, while non-empty text must parse and match
225
+ `json_type`. Plain strings retain existing behavior, including malformed JSON.
226
+ The backend converts supported representations before checking runtime values;
227
+ the CLI only validates configuration and never executes the configured HTTP
228
+ request. Contracts do not configure nested JSON Schema constraints or defaults.
229
+
230
+ Keys come from template paths, not instructions: `order.count` → `order_count`,
231
+ `items[0].id` → `items_0_id`, root string → `body`. Non-ASCII-alphanumeric runs
232
+ become `_`, edge underscores are removed, and keys are lowercased. Multiple
233
+ placeholders in one string add `_1`, `_2`; traversal collisions add `_2`, `_3`.
234
+ Object insertion order matters: preserve it when editing/exporting. Typed and
235
+ JSON-text placeholders must occupy the entire template value. Stale contract
236
+ keys fail validation; omitted entries remain ordinary strings.
237
+
238
+ For an existing Agent:
239
+
240
+ ```bash
241
+ codeer agent get <agent-id> --out .codeer/current/agent.json
242
+ # Prepare local_draft_agent.json from current writable settings; review its diff.
243
+ codeer agent apply --agent-id <agent-id> --payload .codeer/current/local_draft_agent.json --dry-run
244
+ # After approval:
245
+ codeer agent apply --agent-id <agent-id> --payload .codeer/current/local_draft_agent.json
246
+ codeer agent get <agent-id> --out .codeer/current/agent.json
247
+ codeer agent get <agent-id> --history <history-id-from-apply> --out .codeer/current/agent-version.json
248
+ ```
249
+
250
+ The external update uses PATCH, but it is **not a nested partial update**.
251
+ Preserve `name`, `system_prompt`, `use_search`, the complete `unified_tools` list
252
+ (including other tools, templates, auth and `draft_policy`), and the full desired
253
+ contract map. Also preserve description, attachments, suggested questions,
254
+ model settings, handoff and other writable settings. Sending one changed tool
255
+ replaces the list; omitting a contract entry resets that input to ordinary string.
256
+ GET responses and writable payloads have different shapes; reconstruct attachment
257
+ IDs and other absent writable fields from current version evidence as needed.
258
+ Do not apply an update if a current setting cannot be preserved by the CLI. See
259
+ [the skill workflow](../codeer-agent/reference/http-input-contracts.md) for details.
260
+
261
+ Dry-run's `http_inputs` lists tool indexes and each generated key's effective
262
+ `type` / `format` / `json_type`, with `configured: false` for defaults. It excludes
263
+ HTTP URLs, auth, headers, query values, instructions and template content.
264
+ `body_inputs_used` is false for GET/HEAD, whose body inputs are unused at runtime.
265
+ `--full` and `--out` deliberately retain complete nested content, including
266
+ credentials; metadata cleanup is limited to resource-level account fields and
267
+ workspace identity. These exports are not redacted artifacts.
268
+
269
+ Compare the fresh GET and exact version snapshot with the intended tools and
270
+ contracts; the server may materialize omitted defaults. `agent versions --out`
271
+ exports version metadata, not snapshots; use `agent get --history` for a snapshot.
272
+ Apply saves a draft. Publish the verified version separately, after approval,
273
+ using `codeer agent publish --agent <agent-id> --history <history-id>` (preview
274
+ with `--dry-run` first).
275
+
178
276
  ## Upgrade and uninstall
179
277
 
180
278
  Upgrade the CLI:
@@ -200,8 +298,9 @@ Use this pattern during agent lifecycle work:
200
298
 
201
299
  ```bash
202
300
  codeer agent list
203
- codeer history list --agent <agent-id> --limit 50
301
+ codeer history list --agent <agent-id> --has-ai-drafts --limit 50
204
302
  codeer history conversations <history-id> --out .codeer/current/history-<history-id>.json
303
+ codeer history ai-drafts <history-id> --out .codeer/current/ai-drafts-<history-id>.json
205
304
  codeer history create --agent <agent-id> --message "Review this plan" --timeout 240
206
305
  codeer history send <history-id> --message "Use the recommended options" --timeout 240
207
306
  codeer eval run --agent <agent-id> --cases <case-ids> --evaluator <evaluator-id> --out .codeer/eval_run.json
@@ -220,19 +319,55 @@ workspace.
220
319
 
221
320
  Flags:
222
321
 
223
- - `--full` prints bounded extra detail for human inspection. It is still
224
- intended to be safe for LLM context.
322
+ - `--full` prints bounded extra detail for human inspection. Some commands,
323
+ including `agent get`, can expose configuration credentials; `history
324
+ ai-drafts` can expose sensitive conversation text and therefore requires
325
+ `--out`. Use each command's flag description as the output contract, and
326
+ inspect complete artifacts locally without flooding LLM context.
225
327
  - `--out <path>` writes complete diagnostic artifacts to a local file. Use it
226
328
  for raw eval results, full conversation turns, full rubric matrices, and
227
329
  other data that can grow with cases, versions, or turns.
228
330
 
229
- `history conversations` defaults to the workspace-editor management export and
230
- follows all pages automatically. Its stdout is a bounded summary that never
231
- prints tool arguments or results; the `--out` artifact preserves the complete
232
- `history-parts-v1` payload, including persisted tool calls/results,
233
- attachments, feedback, and metadata. System prompts and provider raw traces are
234
- not part of that export contract, and a missing part does not prove that a tool
235
- was not executed.
331
+ `history conversations` reads `/api/v1/external/histories/{id}/messages`
332
+ using a workspace admin API key and follows all pages automatically. Member
333
+ keys retain existing History visibility but are intentionally rejected by this
334
+ complete tool-payload export. This requires a server supporting
335
+ `history-parts-v1`; it never falls back to a
336
+ different authorization contract. Stdout shows at most 20 part summaries (50
337
+ with `--full`) and omits tool payload previews. `--out` retains native tool
338
+ args/results/outcomes, group/part IDs, attachments, feedback, and metadata.
339
+ Attachment URLs remain permission-checked History download endpoints rather
340
+ than direct storage/source URLs.
341
+ Legacy projections have `source: legacy-adapter`; tool outcomes absent from
342
+ the original records are omitted and marked `outcome_not_recorded`. System
343
+ prompts and provider raw traces are not included. Missing parts do not prove a tool never ran. Keep export files private.
344
+
345
+ `--client-visible --user <external-user-id>` explicitly selects the existing
346
+ Chat V2 owner/allowlist contract. No external identity is inferred from History
347
+ metadata. `history get` and the low-level legacy `get_conversations` reader
348
+ remain compatible. Management exports do not hydrate display-only tool payloads.
349
+
350
+ Release order: deploy the backend supporting `history-parts-v1` first, verify
351
+ an authorized management export across multiple pages, then release/install
352
+ this CLI. Existing CLI versions retain their previous behavior until upgraded.
353
+ If the backend endpoint is unavailable, the new CLI fails explicitly with no
354
+ fallback; keep the previous CLI installed until backend verification passes.
355
+ The management endpoint can remain available if the CLI release is rolled back.
356
+
357
+ `history list --has-ai-drafts` narrows the history page to conversations with
358
+ at least one AI Draft and includes lifecycle counts in compact output.
359
+ `history ai-drafts` follows every server page and writes every returned draft
360
+ lifecycle record to `--out`: generated content, refinement lineage,
361
+ `generation_instruction`, `dismiss_reason`, `dismiss_feedback`, outcomes, tool
362
+ activities, proposed actions, operator attribution, and the correlated actual
363
+ delivery when one exists. Default stdout shows structural flags and counts but
364
+ no generated, operator, customer, or tool text. `--full --out <path>` explicitly
365
+ opts into bounded content previews. The endpoint has no revision token, so a
366
+ multi-page artifact is marked `snapshot_consistency: best-effort`: count changes
367
+ and duplicate IDs fail the export, but lifecycle fields can still change during
368
+ paging. Re-run when point-in-time consistency matters. These fields are evidence
369
+ for an improvement analysis; the CLI does not invent a recommended Agent change
370
+ from them.
236
371
 
237
372
  Use the external client-owner contract only when that distinction is the point
238
373
  of the test:
@@ -1,25 +1,26 @@
1
1
  codeer_cli/__init__.py,sha256=z3rkUdVCt11c8QE3-YFkG5cFObM6QhPe2yKugA8k3aA,1915
2
- codeer_cli/_validate.py,sha256=lqMwkhsBy1g0o5CINBp1HdBCjMawgUaEsZygjJaAPL0,6943
3
- codeer_cli/agents.py,sha256=Yt_Rw4c9omsTnXsTe5n_9mD4TpDcx_1WuUkhtKmFY0g,6371
2
+ codeer_cli/_http_contracts.py,sha256=W1YSs9DhWCImDEloin47361qxFNBePuzZKnWRUb16CY,4612
3
+ codeer_cli/_validate.py,sha256=7GEIwLzjP5fPthfsCNtsnqBYiFWqIbuOe_SltrHCE7Q,7302
4
+ codeer_cli/agents.py,sha256=kCvCF7LHQrDOVR80x3i9sWTeC08qvCvR-aUDIMMx5xc,6399
4
5
  codeer_cli/chats.py,sha256=Vg1DIu8fT6mPyPpfqLpExKvXhdZEWCyJGPvalQy3BXg,7989
5
- codeer_cli/cli.py,sha256=PDi0WLC4prkFSbY0Y7619MAdP3uoDzgTWekHrhHprHc,4756
6
- codeer_cli/client.py,sha256=apUycjK-OvHEquAj58FqOy8FHmHjA6K9dScaYQ4M-VU,13175
6
+ codeer_cli/cli.py,sha256=Y5f_HdCAd-gMKlgh49OO-T396f9o_BhpTOFI971NItk,4907
7
+ codeer_cli/client.py,sha256=7fFDAMW5jliMx6PLFovfHDLOLj8z4Uyj5Nm_GdDAr8A,13266
7
8
  codeer_cli/constants.py,sha256=D1pV3wCoqYybrKGKeoupYjjFWLfaFviKp1yL7oh6Qso,2323
8
9
  codeer_cli/eval_.py,sha256=TjDFGCGF19JaUmSaP3PiKIRrBIvYaU-EJS4dC-N7eBI,17868
9
- codeer_cli/histories.py,sha256=Tom4tM20mls-Jo-q7NXish3zeOyMM5Nkd7MZjHFfy88,9979
10
+ codeer_cli/histories.py,sha256=UvFGOMrWeQTh0CjGxBB7rNlRarJrp8kHJVwi9sT7FYI,14592
10
11
  codeer_cli/kb.py,sha256=-HodcGwTvIKgvuRVc5sPFu51si-GlzLEsPSHmnfrVQU,10615
11
12
  codeer_cli/models.py,sha256=Ama9fx8hyRMdysiUNA3Ty0sYIYwzFDkOcAZg-id3-vQ,362
12
13
  codeer_cli/parse.py,sha256=-txMtIxPfeg_i4McEskVKxo8p4438gKS-sFTI38RoBs,21772
13
14
  codeer_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- codeer_cli/commands/_util.py,sha256=X9-9cYgnBYz94hVG4Pe9SsI97DHNv3HE5_7GlPMn93I,1708
15
- codeer_cli/commands/agent.py,sha256=BjctOF0tflt8yVGzdc6320sWXVUAYrF39tNIiTxtQx4,16319
15
+ codeer_cli/commands/_util.py,sha256=AM-ks4Q_HwMlACBEj8pNKqo7yWoOWuQZeHVehy9MwJQ,1915
16
+ codeer_cli/commands/agent.py,sha256=vNPWMboU1pr6pD-8agfP9TjY_UThoJwGKIvshueanig,17471
16
17
  codeer_cli/commands/check.py,sha256=lTxolx1mIJ8jldPhJ5FXqie9nbCLVOO-sDPOHTSy1-w,3817
17
18
  codeer_cli/commands/eval_cmd.py,sha256=S-ydzWZppPLMCu2i_HOK0OW5ZaoebxcLO--WDYOIuAk,73548
18
- codeer_cli/commands/history.py,sha256=jUm0ZnIaJc0FV7pwMNdCBuuKPoauhDY2swFqB6_byWU,18800
19
+ codeer_cli/commands/history.py,sha256=3VqyllZP2PvYvWbgw_GG2kK_VOFtkLndp_RIXuUBm3A,23166
19
20
  codeer_cli/commands/kb.py,sha256=1lwrKTC0NpnAvXZ0u5DPsfx7ZXcHLURyBrqRViv03Yk,42349
20
21
  codeer_cli/commands/model.py,sha256=91LDo_YeHi3POZV7KTCkoCcgtK5p87LwrHX1lZgfCps,1698
21
22
  codeer_cli/commands/profile.py,sha256=IdlXC_6cqobsfN3JRrAnt-1OgBUsIFneS9QtR4Un6Kc,6521
22
- codeer_cli-0.1.15.dist-info/METADATA,sha256=Cwz2jFmIb0GSVClvWt3WqfKm-F_SRvO7W6n67d6t7AY,10673
23
- codeer_cli-0.1.15.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
24
- codeer_cli-0.1.15.dist-info/entry_points.txt,sha256=-nXIrlm5SR5r7gg3y8AS0tN66MwmvNHsrlwLNQNGD50,47
25
- codeer_cli-0.1.15.dist-info/RECORD,,
23
+ codeer_cli-0.1.16.dist-info/METADATA,sha256=jYT-occxU9SDNf_CKXOzwJcaPXzC81_eDFVk4ujSFeU,18263
24
+ codeer_cli-0.1.16.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
25
+ codeer_cli-0.1.16.dist-info/entry_points.txt,sha256=-nXIrlm5SR5r7gg3y8AS0tN66MwmvNHsrlwLNQNGD50,47
26
+ codeer_cli-0.1.16.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any