codeer-cli 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codeer_cli/cli.py CHANGED
@@ -3,7 +3,7 @@
3
3
  codeer check
4
4
  codeer agent list|get|apply|diff|versions
5
5
  codeer kb list|files|upload|faq-list|faq-get|faq-create|faq-update|faq-delete
6
- codeer eval list|evaluators|evaluator-create|evaluator-update|run|export|reconcile|cases-apply|rubrics|rubrics-apply
6
+ 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
7
7
  codeer history list|get|conversations|negative-feedback
8
8
  """
9
9
 
@@ -29,12 +29,17 @@ Safe workflow for coding agents:
29
29
  codeer agent get <agent-id> --full
30
30
  codeer kb list
31
31
  codeer eval list --agent <agent-id>
32
+ codeer eval label-list
33
+ codeer eval case-update --case <case-id> --input "..." --dry-run
32
34
  codeer eval evaluators
33
35
  codeer agent diff --agent <agent-id> --from-version <n> --to-version <n>
34
36
  codeer eval reconcile --agent <agent-id> --manifest .codeer/eval_cases.json
35
37
 
36
38
  Preview mutations before applying:
37
39
  codeer agent apply --payload agent.json --dry-run
40
+ codeer eval case-update --case <case-id> --input "..." --dry-run
41
+ codeer eval label-create --name "routing" --color "#0969da" --dry-run
42
+ codeer eval case-delete --case <case-id> --dry-run
38
43
  codeer eval cases-apply --agent <agent-id> --cases eval_cases.json --dry-run
39
44
  codeer eval rubrics-apply --rubrics rubrics.json --dry-run
40
45
  codeer kb upload --dir kb --name "Product KB" --dry-run
@@ -46,6 +46,72 @@ def register(subparsers):
46
46
  help="Write stripped full case payloads to this file; stdout stays compact unless --full.")
47
47
  p.set_defaults(func=run_list)
48
48
 
49
+ # codeer eval label-list/create/update/delete
50
+ p = sub.add_parser("label-list", help="List eval case labels in the workspace")
51
+ p.add_argument("--workspace", default=None, help="Workspace UUID (default: active API-key workspace)")
52
+ p.add_argument("--out", default=None)
53
+ p.set_defaults(func=run_label_list)
54
+
55
+ p = sub.add_parser("label-create", help="Create an eval case label; run --dry-run first")
56
+ p.add_argument("--name", required=True)
57
+ p.add_argument("--color", default=None, help="Hex color like #0969da (default: server default)")
58
+ p.add_argument("--workspace", default=None, help="Workspace UUID (default: active API-key workspace)")
59
+ p.add_argument("--dry-run", action="store_true")
60
+ p.add_argument("--out", default=None)
61
+ p.set_defaults(func=run_label_create)
62
+
63
+ p = sub.add_parser("label-update", help="Update an eval case label; run --dry-run first")
64
+ p.add_argument("--label", required=True, dest="label_id", help="Eval case label ID")
65
+ p.add_argument("--name", default=None)
66
+ p.add_argument("--color", default=None, help="Hex color like #0969da")
67
+ p.add_argument("--dry-run", action="store_true")
68
+ p.add_argument("--out", default=None)
69
+ p.set_defaults(func=run_label_update)
70
+
71
+ p = sub.add_parser("label-delete", help="Delete an eval case label; run --dry-run first")
72
+ p.add_argument("--label", required=True, dest="label_id", help="Eval case label ID")
73
+ p.add_argument("--dry-run", action="store_true")
74
+ p.add_argument("--out", default=None)
75
+ p.set_defaults(func=run_label_delete)
76
+
77
+ # codeer eval case-update
78
+ p = sub.add_parser("case-update", help="Update one eval case by UUID; run --dry-run first")
79
+ p.add_argument("--case", required=True, dest="case_id", help="Eval case UUID")
80
+ g = p.add_mutually_exclusive_group()
81
+ g.add_argument("--input", help="New eval case input text")
82
+ g.add_argument("--input-file", help="Path to new eval case input text")
83
+ g = p.add_mutually_exclusive_group()
84
+ g.add_argument("--expected-output", help="New expected_output text")
85
+ g.add_argument("--expected-output-file", help="Path to new expected_output text")
86
+ g = p.add_mutually_exclusive_group()
87
+ g.add_argument("--rubric", help="New case-level rubric text")
88
+ g.add_argument("--rubric-file", help="Path to new case-level rubric text")
89
+ g = p.add_mutually_exclusive_group()
90
+ g.add_argument("--note", help="New case note text")
91
+ g.add_argument("--note-file", help="Path to new case note text")
92
+ g = p.add_mutually_exclusive_group()
93
+ g.add_argument("--meta-json", help="New case meta JSON object")
94
+ g.add_argument("--meta-file", help="Path to new case meta JSON object")
95
+ p.add_argument("--attachment-ids", default=None,
96
+ help="Comma-separated file UUIDs to set as the case attachments")
97
+ g = p.add_mutually_exclusive_group()
98
+ g.add_argument("--label-ids", default=None,
99
+ help="Comma-separated eval case label IDs to set on the case")
100
+ g.add_argument("--clear-labels", action="store_true",
101
+ help="Remove all labels from the case")
102
+ p.add_argument("--dry-run", action="store_true",
103
+ help="Validate inputs and print intended mutation without writing server state.")
104
+ p.add_argument("--out", default=None)
105
+ p.set_defaults(func=run_case_update)
106
+
107
+ # codeer eval case-delete
108
+ p = sub.add_parser("case-delete", help="Delete one eval case by UUID; run --dry-run first")
109
+ p.add_argument("--case", required=True, dest="case_id", help="Eval case UUID")
110
+ p.add_argument("--dry-run", action="store_true",
111
+ help="Print intended deletion without writing server state.")
112
+ p.add_argument("--out", default=None)
113
+ p.set_defaults(func=run_case_delete)
114
+
49
115
  # codeer eval evaluators
50
116
  p = sub.add_parser(
51
117
  "evaluators",
@@ -121,6 +187,8 @@ def register(subparsers):
121
187
  p.add_argument("--agent", required=True)
122
188
  p.add_argument("--attachments-dir", default=None, dest="attachments_dir")
123
189
  p.add_argument("--allow-duplicates", action="store_true")
190
+ p.add_argument("--create-labels", action="store_true",
191
+ help="Create missing labels referenced by manifest case labels.")
124
192
  p.add_argument("--dry-run", action="store_true",
125
193
  help="Validate manifest and print intended mutations without writing server state.")
126
194
  p.add_argument("--out", default=None)
@@ -151,6 +219,7 @@ def register(subparsers):
151
219
  # ---------------------------------------------------------------------------
152
220
 
153
221
  def _case_summary(case: dict, *, full: bool = False) -> dict:
222
+ labels = case.get("labels") or []
154
223
  row = {
155
224
  "id": case.get("id"),
156
225
  "input_preview": truncate(case.get("input") or "", 240 if full else 80),
@@ -158,6 +227,11 @@ def _case_summary(case: dict, *, full: bool = False) -> dict:
158
227
  "expected_output_chars": len(case.get("expected_output") or ""),
159
228
  "note_preview": truncate(case.get("note") or "", 180 if full else 100),
160
229
  "attachment_count": len(case.get("attachments") or case.get("attachment_ids") or []),
230
+ "labels": [
231
+ {"id": label.get("id"), "name": label.get("name"), "color": label.get("color")}
232
+ for label in labels
233
+ if isinstance(label, dict)
234
+ ],
161
235
  }
162
236
  if full:
163
237
  row["created_at"] = case.get("created_at")
@@ -206,6 +280,232 @@ def run_list(args, client) -> int:
206
280
  return 0
207
281
 
208
282
 
283
+ # ---------------------------------------------------------------------------
284
+ # eval case labels
285
+ # ---------------------------------------------------------------------------
286
+
287
+ def _workspace_arg_or_default(client, workspace_id: str | None) -> str:
288
+ if workspace_id:
289
+ return workspace_id
290
+ ws, _ = client.resolve_scope()
291
+ return ws
292
+
293
+
294
+ def _label_summary(label: dict) -> dict:
295
+ return {
296
+ "id": label.get("id"),
297
+ "name": label.get("name"),
298
+ "color": label.get("color"),
299
+ "workspace_id": label.get("workspace_id"),
300
+ }
301
+
302
+
303
+ def run_label_list(args, client) -> int:
304
+ workspace_id = _workspace_arg_or_default(client, args.workspace)
305
+ labels = eval_mod.list_case_labels(client, workspace_id=workspace_id)
306
+ out = {
307
+ "workspace_id": workspace_id,
308
+ "label_count": len(labels),
309
+ "labels": [_label_summary(label) for label in labels],
310
+ }
311
+ print_json(out)
312
+ write_json(args.out, out)
313
+ return 0
314
+
315
+
316
+ def run_label_create(args, client) -> int:
317
+ workspace_id = _workspace_arg_or_default(client, args.workspace)
318
+ if args.dry_run:
319
+ out = {
320
+ "dry_run": True,
321
+ "operation": "label_create",
322
+ "method": "POST",
323
+ "path": f"/eval/workspaces/{workspace_id}/case-labels",
324
+ "workspace_id": workspace_id,
325
+ "name": args.name,
326
+ "color": args.color,
327
+ "would_write_server_state": True,
328
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
329
+ }
330
+ print_json(out)
331
+ write_json(args.out, out)
332
+ return 0
333
+
334
+ label = eval_mod.create_case_label(
335
+ client, workspace_id=workspace_id, name=args.name, color=args.color
336
+ )
337
+ out = _label_summary(strip_noisy_fields(label))
338
+ print_json(out)
339
+ write_json(args.out, out)
340
+ return 0
341
+
342
+
343
+ def run_label_update(args, client) -> int:
344
+ if args.name is None and args.color is None:
345
+ log("error: provide --name and/or --color")
346
+ return 2
347
+
348
+ if args.dry_run:
349
+ out = {
350
+ "dry_run": True,
351
+ "operation": "label_update",
352
+ "method": "PUT",
353
+ "path": f"/eval/case-labels/{args.label_id}",
354
+ "label_id": args.label_id,
355
+ "updates": {"name": args.name, "color": args.color},
356
+ "would_write_server_state": True,
357
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
358
+ }
359
+ print_json(out)
360
+ write_json(args.out, out)
361
+ return 0
362
+
363
+ label = eval_mod.update_case_label(
364
+ client, label_id=args.label_id, name=args.name, color=args.color
365
+ )
366
+ out = _label_summary(strip_noisy_fields(label))
367
+ print_json(out)
368
+ write_json(args.out, out)
369
+ return 0
370
+
371
+
372
+ def run_label_delete(args, client) -> int:
373
+ if args.dry_run:
374
+ out = {
375
+ "dry_run": True,
376
+ "operation": "label_delete",
377
+ "method": "DELETE",
378
+ "path": f"/eval/case-labels/{args.label_id}",
379
+ "label_id": args.label_id,
380
+ "would_write_server_state": True,
381
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
382
+ }
383
+ print_json(out)
384
+ write_json(args.out, out)
385
+ return 0
386
+
387
+ deleted = strip_noisy_fields(eval_mod.delete_case_label(client, label_id=args.label_id))
388
+ print_json(deleted)
389
+ write_json(args.out, deleted)
390
+ return 0
391
+
392
+
393
+ # ---------------------------------------------------------------------------
394
+ # eval case-update / case-delete
395
+ # ---------------------------------------------------------------------------
396
+
397
+ def _read_text_arg(value: str | None, file_path: str | None) -> str | None:
398
+ if file_path is not None:
399
+ return Path(file_path).read_text()
400
+ return value
401
+
402
+
403
+ def _read_meta_arg(value: str | None, file_path: str | None) -> dict | None:
404
+ if file_path is not None:
405
+ raw = Path(file_path).read_text()
406
+ elif value is not None:
407
+ raw = value
408
+ else:
409
+ return None
410
+
411
+ meta = json.loads(raw)
412
+ if not isinstance(meta, dict):
413
+ raise ValueError("case meta must be a JSON object")
414
+ return meta
415
+
416
+
417
+ def run_case_update(args, client) -> int:
418
+ try:
419
+ input_text = _read_text_arg(args.input, args.input_file)
420
+ expected_output = _read_text_arg(args.expected_output, args.expected_output_file)
421
+ rubric = _read_text_arg(args.rubric, args.rubric_file)
422
+ note = _read_text_arg(args.note, args.note_file)
423
+ meta = _read_meta_arg(args.meta_json, args.meta_file)
424
+ except (OSError, json.JSONDecodeError, ValueError) as e:
425
+ log(f"error: {e}")
426
+ return 2
427
+ attachment_ids = _ids(args.attachment_ids)
428
+ label_ids = [] if args.clear_labels else _ids(args.label_ids)
429
+
430
+ has_update = any(
431
+ value is not None
432
+ for value in (input_text, expected_output, rubric, note, meta, attachment_ids, label_ids)
433
+ ) or args.clear_labels
434
+ if not has_update:
435
+ log(
436
+ "error: provide at least one of --input, --input-file, --expected-output, "
437
+ "--expected-output-file, --rubric, --rubric-file, --note, --note-file, "
438
+ "--meta-json, --meta-file, --attachment-ids, --label-ids, --clear-labels"
439
+ )
440
+ return 2
441
+
442
+ if args.dry_run:
443
+ current = strip_noisy_fields(eval_mod.get_case(client, args.case_id))
444
+ out = {
445
+ "dry_run": True,
446
+ "operation": "case_update",
447
+ "method": "PUT",
448
+ "path": f"/external/eval/cases/{args.case_id}",
449
+ "case_id": args.case_id,
450
+ "current": _case_summary(current, full=True),
451
+ "updates": {
452
+ "input_chars": len(input_text) if input_text is not None else None,
453
+ "expected_output_chars": (
454
+ len(expected_output) if expected_output is not None else None
455
+ ),
456
+ "rubric_chars": len(rubric) if rubric is not None else None,
457
+ "note_chars": len(note) if note is not None else None,
458
+ "meta": meta,
459
+ "attachment_ids": attachment_ids,
460
+ "label_ids": label_ids,
461
+ },
462
+ "would_write_server_state": True,
463
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
464
+ }
465
+ print_json(out)
466
+ write_json(args.out, out)
467
+ return 0
468
+
469
+ updated = eval_mod.update_case(
470
+ client,
471
+ args.case_id,
472
+ input=input_text,
473
+ expected_output=expected_output,
474
+ rubric=rubric,
475
+ attachment_ids=attachment_ids,
476
+ label_ids=label_ids,
477
+ meta=meta,
478
+ note=note,
479
+ )
480
+ out = strip_noisy_fields(updated)
481
+ print_json(out)
482
+ write_json(args.out, out)
483
+ return 0
484
+
485
+
486
+ def run_case_delete(args, client) -> int:
487
+ if args.dry_run:
488
+ current = strip_noisy_fields(eval_mod.get_case(client, args.case_id))
489
+ out = {
490
+ "dry_run": True,
491
+ "operation": "case_delete",
492
+ "method": "DELETE",
493
+ "path": f"/external/eval/cases/{args.case_id}",
494
+ "case_id": args.case_id,
495
+ "current": _case_summary(current, full=True),
496
+ "would_write_server_state": True,
497
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
498
+ }
499
+ print_json(out)
500
+ write_json(args.out, out)
501
+ return 0
502
+
503
+ deleted = strip_noisy_fields(eval_mod.delete_case(client, args.case_id))
504
+ print_json(deleted)
505
+ write_json(args.out, deleted)
506
+ return 0
507
+
508
+
209
509
  # ---------------------------------------------------------------------------
210
510
  # eval evaluators
211
511
  # ---------------------------------------------------------------------------
@@ -795,6 +1095,50 @@ def _upload_attachment(client: CodeerClient, *, file_path: Path, workspace_id: s
795
1095
  return uuid
796
1096
 
797
1097
 
1098
+ def _manifest_label_names(case: dict) -> list[str]:
1099
+ raw = case.get("labels")
1100
+ if raw is None:
1101
+ return []
1102
+ if not isinstance(raw, list) or not all(isinstance(item, str) for item in raw):
1103
+ raise ValueError(f"case '{case.get('label')}' labels must be a list of label names")
1104
+ return [item.strip() for item in raw if item.strip()]
1105
+
1106
+
1107
+ def _manifest_label_ids(case: dict) -> list[str] | None:
1108
+ raw = case.get("label_ids")
1109
+ if raw is None:
1110
+ return None
1111
+ if not isinstance(raw, list) or not all(isinstance(item, str) for item in raw):
1112
+ raise ValueError(f"case '{case.get('label')}' label_ids must be a list of label ID strings")
1113
+ return [item.strip() for item in raw if item.strip()]
1114
+
1115
+
1116
+ def _dedupe_preserve_order(items: list[str]) -> list[str]:
1117
+ seen: set[str] = set()
1118
+ out: list[str] = []
1119
+ for item in items:
1120
+ if item in seen:
1121
+ continue
1122
+ seen.add(item)
1123
+ out.append(item)
1124
+ return out
1125
+
1126
+
1127
+ def _resolve_case_label_ids(case: dict, labels_by_name: dict[str, dict]) -> tuple[list[str] | None, list[str]]:
1128
+ explicit_ids = _manifest_label_ids(case)
1129
+ label_names = _manifest_label_names(case)
1130
+ if explicit_ids is None and not label_names:
1131
+ return None, []
1132
+
1133
+ resolved_ids = list(explicit_ids or [])
1134
+ for name in label_names:
1135
+ label = labels_by_name.get(name.casefold())
1136
+ if label is None:
1137
+ raise ValueError(f"case '{case.get('label')}' references unknown label '{name}'")
1138
+ resolved_ids.append(str(label["id"]))
1139
+ return _dedupe_preserve_order(resolved_ids), label_names
1140
+
1141
+
798
1142
  def run_cases_apply(args, client) -> int:
799
1143
  payload = json.loads(Path(args.cases).read_text())
800
1144
  cases = payload.get("cases") or []
@@ -815,6 +1159,53 @@ def run_cases_apply(args, client) -> int:
815
1159
  return 2
816
1160
 
817
1161
  workspace_id, _ = client.resolve_scope()
1162
+ try:
1163
+ manifest_label_names = sorted({
1164
+ name
1165
+ for case in cases
1166
+ for name in _manifest_label_names(case)
1167
+ }, key=str.casefold)
1168
+ for case in cases:
1169
+ _manifest_label_ids(case)
1170
+ except ValueError as e:
1171
+ log(f"error: {e}")
1172
+ return 2
1173
+
1174
+ labels_by_name: dict[str, dict] = {}
1175
+ created_labels: list[dict] = []
1176
+ would_create_labels: list[str] = []
1177
+ if manifest_label_names:
1178
+ labels_by_name = {
1179
+ (label.get("name") or "").casefold(): label
1180
+ for label in eval_mod.list_case_labels(client, workspace_id=workspace_id)
1181
+ if label.get("name")
1182
+ }
1183
+ missing_label_names = [
1184
+ name for name in manifest_label_names
1185
+ if name.casefold() not in labels_by_name
1186
+ ]
1187
+ if missing_label_names and not args.create_labels:
1188
+ log(
1189
+ "error: manifest references missing labels: "
1190
+ + ", ".join(missing_label_names)
1191
+ + ". Create them first with `codeer eval label-create`, "
1192
+ + "or rerun cases-apply with --create-labels."
1193
+ )
1194
+ return 2
1195
+ if args.dry_run:
1196
+ would_create_labels = missing_label_names
1197
+ for name in missing_label_names:
1198
+ labels_by_name[name.casefold()] = {
1199
+ "id": f"(new:{name})",
1200
+ "name": name,
1201
+ "color": "#0969da",
1202
+ }
1203
+ else:
1204
+ for name in missing_label_names:
1205
+ log(f"creating label: {name}")
1206
+ label = eval_mod.create_case_label(client, workspace_id=workspace_id, name=name)
1207
+ labels_by_name[name.casefold()] = label
1208
+ created_labels.append(_label_summary(label))
818
1209
 
819
1210
  existing_by_input: dict[str, dict] = {}
820
1211
  if not args.allow_duplicates:
@@ -838,6 +1229,11 @@ def run_cases_apply(args, client) -> int:
838
1229
  return 2
839
1230
 
840
1231
  label = case.get("label", "(unlabeled)")
1232
+ try:
1233
+ case_label_ids, case_label_names = _resolve_case_label_ids(case, labels_by_name)
1234
+ except ValueError as e:
1235
+ log(f"error: {e}")
1236
+ return 2
841
1237
  attachment_ids: list[str] = []
842
1238
  for fname in case.get("attachment_files") or []:
843
1239
  fp = (attach_dir / fname).resolve() if attach_dir else None
@@ -866,16 +1262,26 @@ def run_cases_apply(args, client) -> int:
866
1262
  or attachment_ids
867
1263
  or case.get("meta") is not None
868
1264
  or case.get("note") is not None
1265
+ or case_label_ids is not None
869
1266
  ),
1267
+ "labels": case_label_names,
1268
+ "label_ids": case_label_ids,
870
1269
  "rubric_count": len(rubrics),
871
1270
  })
872
1271
  continue
873
1272
  log(f"reusing existing case: {label} ({case_id[:8]})")
874
- if case.get("expected_output") is not None or attachment_ids or case.get("meta") is not None or case.get("note") is not None:
1273
+ if (
1274
+ case.get("expected_output") is not None
1275
+ or attachment_ids
1276
+ or case.get("meta") is not None
1277
+ or case.get("note") is not None
1278
+ or case_label_ids is not None
1279
+ ):
875
1280
  eval_mod.update_case(
876
1281
  client, case_id,
877
1282
  expected_output=case.get("expected_output"),
878
1283
  attachment_ids=attachment_ids or None,
1284
+ label_ids=case_label_ids,
879
1285
  meta=case.get("meta"),
880
1286
  note=case.get("note"),
881
1287
  )
@@ -894,6 +1300,8 @@ def run_cases_apply(args, client) -> int:
894
1300
  "input_chars": len(case.get("input") or ""),
895
1301
  "expected_output_chars": len(case.get("expected_output") or ""),
896
1302
  "attachment_count": len(attachment_ids),
1303
+ "labels": case_label_names,
1304
+ "label_ids": case_label_ids,
897
1305
  "rubric_count": len(rubrics),
898
1306
  })
899
1307
  continue
@@ -903,6 +1311,7 @@ def run_cases_apply(args, client) -> int:
903
1311
  client, agent_id=args.agent, input=case["input"],
904
1312
  expected_output=case.get("expected_output"),
905
1313
  attachment_ids=attachment_ids or None,
1314
+ label_ids=case_label_ids,
906
1315
  rubrics_by_evaluator=rubrics, meta=case.get("meta"),
907
1316
  note=case.get("note"),
908
1317
  )
@@ -910,12 +1319,19 @@ def run_cases_apply(args, client) -> int:
910
1319
  labels.append(label)
911
1320
  created.append({"case_id": result["id"], "label": label})
912
1321
 
913
- out = {"case_ids": case_ids, "labels": labels, "created": created, "reused": reused}
1322
+ out = {
1323
+ "case_ids": case_ids,
1324
+ "labels": labels,
1325
+ "created": created,
1326
+ "reused": reused,
1327
+ "created_case_labels": created_labels,
1328
+ }
914
1329
  if args.dry_run:
915
1330
  out.update({
916
1331
  "dry_run": True,
917
1332
  "operation": "cases_apply",
918
1333
  "agent_id": args.agent,
1334
+ "would_create_case_labels": would_create_labels,
919
1335
  "updates": dry_run_updates,
920
1336
  "would_write_server_state": True,
921
1337
  "next_step": "Review this summary, then rerun without --dry-run after approval.",
codeer_cli/commands/kb.py CHANGED
@@ -107,8 +107,7 @@ def register(subparsers):
107
107
  p.add_argument("--context-object-id", type=int, required=True,
108
108
  help="KB file snapshot_object_id from `codeer kb files`")
109
109
  p.add_argument("--question", required=True)
110
- p.add_argument("--range", dest="ranges", action="append", type=_parse_faq_range, default=None,
111
- help="Reserve matching chunks that overlap START_LINE:END_LINE; repeatable")
110
+ _add_faq_range_args(p)
112
111
  p.add_argument("--dry-run", action="store_true",
113
112
  help="Print intended request without writing server state.")
114
113
  p.add_argument("--out", default=None, help="Write result JSON to this file too")
@@ -119,8 +118,7 @@ def register(subparsers):
119
118
  p.add_argument("--context-object-id", type=int, default=None,
120
119
  help="Move FAQ to a different KB file snapshot_object_id")
121
120
  p.add_argument("--question", default=None)
122
- p.add_argument("--range", dest="ranges", action="append", type=_parse_faq_range, default=None,
123
- help="Replace reserved ranges with START_LINE:END_LINE; repeatable")
121
+ _add_faq_range_args(p, verb="Replace")
124
122
  p.add_argument("--dry-run", action="store_true",
125
123
  help="Print intended request without writing server state.")
126
124
  p.add_argument("--out", default=None, help="Write result JSON to this file too")
@@ -217,18 +215,50 @@ def _dry_run(path: str | None, result: dict) -> int:
217
215
  return 0
218
216
 
219
217
 
218
+ def _add_faq_range_args(parser, *, verb: str = "Reserve") -> None:
219
+ parser.add_argument(
220
+ "--range",
221
+ dest="ranges",
222
+ action="append",
223
+ type=_parse_faq_range,
224
+ default=None,
225
+ help=(
226
+ f"{verb} matching passages as "
227
+ "START_LINE:START_COLUMN-END_LINE:END_COLUMN; repeatable"
228
+ ),
229
+ )
230
+
231
+
220
232
  def _parse_faq_range(value: str) -> dict[str, int]:
221
233
  try:
222
- start_raw, end_raw = value.split(":", 1)
223
- start_line = int(start_raw)
224
- end_line = int(end_raw)
234
+ start_raw, end_raw = value.split("-", 1)
235
+ start_line_raw, start_column_raw = start_raw.split(":", 1)
236
+ end_line_raw, end_column_raw = end_raw.split(":", 1)
237
+ faq_range = {
238
+ "start_line": int(start_line_raw),
239
+ "start_column": int(start_column_raw),
240
+ "end_line": int(end_line_raw),
241
+ "end_column": int(end_column_raw),
242
+ }
225
243
  except ValueError as exc:
226
- raise argparse.ArgumentTypeError("expected START_LINE:END_LINE") from exc
244
+ raise argparse.ArgumentTypeError(
245
+ "expected START_LINE:START_COLUMN-END_LINE:END_COLUMN"
246
+ ) from exc
247
+ _validate_faq_range_position(faq_range)
248
+ return faq_range
249
+
250
+
251
+ def _validate_faq_range_position(faq_range: dict[str, int]) -> None:
252
+ start_line = faq_range["start_line"]
253
+ end_line = faq_range["end_line"]
254
+ start_column = faq_range["start_column"]
255
+ end_column = faq_range["end_column"]
227
256
  if start_line < 1 or end_line < 1:
228
257
  raise argparse.ArgumentTypeError("line numbers must be >= 1")
229
- if end_line < start_line:
230
- raise argparse.ArgumentTypeError("END_LINE must be >= START_LINE")
231
- return {"start_line": start_line, "end_line": end_line}
258
+ if start_column < 0 or end_column < 0:
259
+ raise argparse.ArgumentTypeError("column numbers must be >= 0")
260
+ if (end_line, end_column) < (start_line, start_column):
261
+ raise argparse.ArgumentTypeError("end position must be >= start position")
232
262
 
233
263
 
234
264
  def _parse_config_json(config_json: str | None) -> dict | None:
codeer_cli/eval_.py CHANGED
@@ -22,6 +22,7 @@ def create_case(
22
22
  expected_output: Optional[str] = None,
23
23
  rubric: Optional[str] = None,
24
24
  attachment_ids: Optional[List[str]] = None,
25
+ label_ids: Optional[List[str]] = None,
25
26
  meta: Optional[dict] = None,
26
27
  note: Optional[str] = None,
27
28
  ) -> dict:
@@ -41,6 +42,8 @@ def create_case(
41
42
  body["rubric"] = rubric
42
43
  if attachment_ids:
43
44
  body["attachment_ids"] = attachment_ids
45
+ if label_ids is not None:
46
+ body["label_ids"] = label_ids
44
47
  if meta:
45
48
  body["meta"] = meta
46
49
  if note is not None:
@@ -64,6 +67,7 @@ def update_case(
64
67
  expected_output: Optional[str] = None,
65
68
  rubric: Optional[str] = None,
66
69
  attachment_ids: Optional[List[str]] = None,
70
+ label_ids: Optional[List[str]] = None,
67
71
  meta: Optional[dict] = None,
68
72
  note: Optional[str] = None,
69
73
  ) -> dict:
@@ -76,6 +80,8 @@ def update_case(
76
80
  body["rubric"] = rubric
77
81
  if attachment_ids is not None:
78
82
  body["attachment_ids"] = attachment_ids
83
+ if label_ids is not None:
84
+ body["label_ids"] = label_ids
79
85
  if meta is not None:
80
86
  body["meta"] = meta
81
87
  if note is not None:
@@ -83,6 +89,48 @@ def update_case(
83
89
  return client.put(f"/external/eval/cases/{case_id}", json=body)
84
90
 
85
91
 
92
+ def delete_case(client: CodeerClient, case_id: str) -> dict:
93
+ return client.delete(f"/external/eval/cases/{case_id}")
94
+
95
+
96
+ # --- case labels -----------------------------------------------------------
97
+
98
+ def list_case_labels(client: CodeerClient, *, workspace_id: str) -> list[dict]:
99
+ return client.get(f"/eval/workspaces/{workspace_id}/case-labels")
100
+
101
+
102
+ def create_case_label(
103
+ client: CodeerClient,
104
+ *,
105
+ workspace_id: str,
106
+ name: str,
107
+ color: Optional[str] = None,
108
+ ) -> dict:
109
+ body: dict[str, Any] = {"name": name}
110
+ if color is not None:
111
+ body["color"] = color
112
+ return client.post(f"/eval/workspaces/{workspace_id}/case-labels", json=body)
113
+
114
+
115
+ def update_case_label(
116
+ client: CodeerClient,
117
+ *,
118
+ label_id: str,
119
+ name: Optional[str] = None,
120
+ color: Optional[str] = None,
121
+ ) -> dict:
122
+ body: dict[str, Any] = {}
123
+ if name is not None:
124
+ body["name"] = name
125
+ if color is not None:
126
+ body["color"] = color
127
+ return client.put(f"/eval/case-labels/{label_id}", json=body)
128
+
129
+
130
+ def delete_case_label(client: CodeerClient, *, label_id: str) -> dict:
131
+ return client.delete(f"/eval/case-labels/{label_id}")
132
+
133
+
86
134
 
87
135
  # --- evaluators -----------------------------------------------------------
88
136
 
@@ -393,6 +441,7 @@ def create_case_with_rubrics(
393
441
  rubrics_by_evaluator: dict[str, str],
394
442
  expected_output: Optional[str] = None,
395
443
  attachment_ids: Optional[List[str]] = None,
444
+ label_ids: Optional[List[str]] = None,
396
445
  meta: Optional[dict] = None,
397
446
  note: Optional[str] = None,
398
447
  ) -> dict:
@@ -412,6 +461,7 @@ def create_case_with_rubrics(
412
461
  input=input,
413
462
  expected_output=expected_output,
414
463
  attachment_ids=attachment_ids,
464
+ label_ids=label_ids,
415
465
  meta=meta,
416
466
  note=note,
417
467
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeer-cli
3
- Version: 0.1.3
3
+ Version: 0.1.5
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
@@ -184,15 +184,19 @@ override matching JSON keys.
184
184
 
185
185
  Use Context Object FAQ entries to route high-value questions to a canonical KB
186
186
  file when semantic retrieval misses the right source. The FAQ target is a KB
187
- file's `snapshot_object_id`, shown by `codeer kb files`. Add `--range
188
- START_LINE:END_LINE` when the route should reserve chunks overlapping a stable
189
- line range inside that file.
187
+ file's `snapshot_object_id`, shown by `codeer kb files`. Add `--range` when the
188
+ route should reserve a stable passage inside that file. Ranges must include both
189
+ line and column positions so the Codeer UI can map them onto rendered Markdown.
190
190
 
191
191
  ```bash
192
192
  codeer kb files --kb-id <kb-id>
193
193
  codeer kb faq-list --context-object-id <snapshot-object-id>
194
- codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:18 --dry-run
194
+ codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:0-12:42 --dry-run
195
+ codeer kb faq-update <faq-id> --range 12:0-12:42 --dry-run
195
196
  ```
196
197
 
198
+ `--range` accepts `START_LINE:START_COLUMN-END_LINE:END_COLUMN`; repeat it to
199
+ reserve multiple passages.
200
+
197
201
  After reviewing the dry-run output, rerun the create/update/delete command
198
202
  without `--dry-run` to apply it.
@@ -2,10 +2,10 @@ codeer_cli/__init__.py,sha256=-0gL8upoSsLAnXAfcRrwqZYJbwG0knzQoFf94O7Nc7c,1817
2
2
  codeer_cli/_validate.py,sha256=pKUJa2TyTpERx5xmiYNZRn7tFqDxLZ2fF1rHAf1oz14,5415
3
3
  codeer_cli/agents.py,sha256=diodgiGhXlowEi8sbCzcSK1qSeCLF2fBe6QBs3Sq_x8,5617
4
4
  codeer_cli/chats.py,sha256=YVrZJhoa-d67o6tzX6riGXsbA-ehyhOxrZ8zRCcJNro,2675
5
- codeer_cli/cli.py,sha256=kaXTCBfzq64fLJDqKH39cN0ds7qv-F-eRxzUsGmCvv0,3901
5
+ codeer_cli/cli.py,sha256=1sdsWJxpAQhz9UrKGUi4EGoBBkvrPUvj8aiimnllFro,4259
6
6
  codeer_cli/client.py,sha256=LpHVqf1IYNg1wFfIHnO9q4xg2h3IiGOitzCnvwB-Bcw,9809
7
7
  codeer_cli/constants.py,sha256=D1pV3wCoqYybrKGKeoupYjjFWLfaFviKp1yL7oh6Qso,2323
8
- codeer_cli/eval_.py,sha256=EsH8f8nT8x9MFlUhpHWSAU4aNPkNceD-Tw5GM15Ae1I,14157
8
+ codeer_cli/eval_.py,sha256=z9RXFiYXOEHPKIh31nbooNe_WmlJ-L63GWBDw3CJhmM,15614
9
9
  codeer_cli/histories.py,sha256=tk28git_peX4x703CIDU8u72JtlGaytyrtlHfxlK-7A,5979
10
10
  codeer_cli/kb.py,sha256=--0MvZJ2OsIbzLh-4TQ-wR7dVK42eaXw-L0qDGFOaxg,10417
11
11
  codeer_cli/parse.py,sha256=qrjZn0MUTjGfucp4cwxy8Pt7WS-0x15kK5F7kWTY8Ps,21818
@@ -13,11 +13,11 @@ codeer_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
13
13
  codeer_cli/commands/_util.py,sha256=VOB_HMWYzHFNY1ElLOED1HB6fpFpsniqH6Yx3VUlMrY,1644
14
14
  codeer_cli/commands/agent.py,sha256=amvfVVrbPOkbYKCGvA6EJB30C-aY6WSdfR7u7FklXbs,14793
15
15
  codeer_cli/commands/check.py,sha256=lTxolx1mIJ8jldPhJ5FXqie9nbCLVOO-sDPOHTSy1-w,3817
16
- codeer_cli/commands/eval_cmd.py,sha256=yvqjPXMOE7V0Hv53iEW5HvIo610dgiIG4BpKGWzZY4I,46965
16
+ codeer_cli/commands/eval_cmd.py,sha256=3Fi_dwpoJ2hceitACunPtR2kVZ84WT2r_CyL-MaJaMw,62902
17
17
  codeer_cli/commands/history.py,sha256=Jv7t0GhSZcbZ8OuIXZT34CixXt7ECEVP3nZ-WW_Ya9E,12026
18
- codeer_cli/commands/kb.py,sha256=vXwgiGHYrLywm3O41-D__7m3d1uZUcw3YYuxKH-08i0,24528
18
+ codeer_cli/commands/kb.py,sha256=r93lKTlQAQ5gdiJpfEptx7l3ue589_k-p386DruT7eI,25295
19
19
  codeer_cli/commands/profile.py,sha256=IdlXC_6cqobsfN3JRrAnt-1OgBUsIFneS9QtR4Un6Kc,6521
20
- codeer_cli-0.1.3.dist-info/METADATA,sha256=lI76CTicsBTRGXZStaOs6I5kK98Pwf7-PV3XtIlxFsA,5291
21
- codeer_cli-0.1.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
22
- codeer_cli-0.1.3.dist-info/entry_points.txt,sha256=-nXIrlm5SR5r7gg3y8AS0tN66MwmvNHsrlwLNQNGD50,47
23
- codeer_cli-0.1.3.dist-info/RECORD,,
20
+ codeer_cli-0.1.5.dist-info/METADATA,sha256=F8WrVVhh8XbCBe6QNdnrxYO7mS6rGw_C58nf7as9jlY,5524
21
+ codeer_cli-0.1.5.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
22
+ codeer_cli-0.1.5.dist-info/entry_points.txt,sha256=-nXIrlm5SR5r7gg3y8AS0tN66MwmvNHsrlwLNQNGD50,47
23
+ codeer_cli-0.1.5.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any