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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codeer_cli/agents.py CHANGED
@@ -153,3 +153,8 @@ def get_version(client: CodeerClient, agent_id: str, history_id: str) -> dict:
153
153
  def check_impact(client: CodeerClient, agent_id: str) -> dict:
154
154
  """List downstream agents that call this one. Call before publishing breaking changes."""
155
155
  return client.get(f"/external/agents/{agent_id}/impact")
156
+
157
+
158
+ def publish_version(client: CodeerClient, agent_id: str, history_id: str) -> dict:
159
+ """Promote one AgentHistory version to the published runtime version."""
160
+ return client.post(f"/external/agents/{agent_id}/versions/{history_id}:publish", json={})
codeer_cli/client.py CHANGED
@@ -85,8 +85,7 @@ class CodeerClient:
85
85
  if not api_key:
86
86
  raise AuthError(
87
87
  0,
88
- "Missing API key. Export CODEER_API_KEY or run `codeer profile add <name>` "
89
- "and `codeer profile use <name>`.",
88
+ "Missing API key. Export CODEER_API_KEY or run `codeer profile add <name>`.",
90
89
  )
91
90
 
92
91
  overrides.pop("workspace_id", None)
@@ -69,6 +69,21 @@ def register(subparsers):
69
69
  help="Write stripped full version snapshots to this file; stdout stays compact.")
70
70
  p.set_defaults(func=run_versions)
71
71
 
72
+ p = sub.add_parser("impact", help="Check downstream agents affected by this agent")
73
+ p.add_argument("--agent", required=True)
74
+ p.add_argument("--out", default=None, help="Write full impact detail to this file too")
75
+ p.set_defaults(func=run_impact)
76
+
77
+ p = sub.add_parser("publish", help="Publish an agent version; run --dry-run first")
78
+ p.add_argument("--agent", required=True)
79
+ g = p.add_mutually_exclusive_group(required=True)
80
+ g.add_argument("--history", default=None, help="AgentHistory UUID to publish")
81
+ g.add_argument("--version", type=int, default=None, help="AgentHistory version_number to publish")
82
+ p.add_argument("--dry-run", action="store_true",
83
+ help="Resolve target version and print intended mutation without writing server state.")
84
+ p.add_argument("--out", default=None, help="Write result JSON to this file too")
85
+ p.set_defaults(func=run_publish)
86
+
72
87
 
73
88
  def _tool_summary(tools: list[dict] | None) -> list[dict]:
74
89
  out = []
@@ -229,6 +244,62 @@ def run_versions(args, client) -> int:
229
244
  return 0
230
245
 
231
246
 
247
+ def run_impact(args, client) -> int:
248
+ result = strip_noisy_fields(agents_mod.check_impact(client, args.agent))
249
+ write_json(args.out, result)
250
+ print_json(result)
251
+ return 0
252
+
253
+
254
+ def _resolve_history_for_publish(
255
+ client: CodeerClient,
256
+ agent_id: str,
257
+ history_id: Optional[str],
258
+ version: Optional[int],
259
+ ) -> dict:
260
+ if history_id:
261
+ return agents_mod.get_version(client, agent_id, history_id)
262
+ if version is not None:
263
+ for candidate in agents_mod.list_versions(client, agent_id):
264
+ if candidate.get("version_number") == version:
265
+ return agents_mod.get_version(client, agent_id, candidate["id"])
266
+ raise SystemExit(f"no version {version} on agent {agent_id}")
267
+ raise SystemExit("must pass --history or --version")
268
+
269
+
270
+ def run_publish(args, client) -> int:
271
+ history = _resolve_history_for_publish(client, args.agent, args.history, args.version)
272
+ history_id = history["id"]
273
+ summary = {
274
+ "agent_id": args.agent,
275
+ "history_id": history_id,
276
+ "version_number": history.get("version_number"),
277
+ "status": history.get("status"),
278
+ "was_published": history.get("was_published"),
279
+ "version_note": history.get("version_note") or "",
280
+ }
281
+
282
+ if args.dry_run:
283
+ result = {
284
+ "dry_run": True,
285
+ "operation": "agent_publish",
286
+ "method": "POST",
287
+ "path": f"/external/agents/{args.agent}/versions/{history_id}:publish",
288
+ "target": summary,
289
+ "would_write_server_state": True,
290
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
291
+ }
292
+ print_json(result)
293
+ write_json(args.out, result)
294
+ return 0
295
+
296
+ result = strip_noisy_fields(agents_mod.publish_version(client, args.agent, history_id))
297
+ output = {"target": summary, "response": result}
298
+ print_json(output)
299
+ write_json(args.out, output)
300
+ return 0
301
+
302
+
232
303
 
233
304
  # --- diff helpers ---
234
305
 
codeer_cli/commands/kb.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import argparse
3
4
  import json
4
5
  import time
5
6
  from pathlib import Path
@@ -11,6 +12,42 @@ POLL_INTERVAL = 3
11
12
  POLL_TIMEOUT = 600
12
13
 
13
14
 
15
+ def _add_crawl_config_args(parser):
16
+ parser.add_argument("--config-json", default=None, help="JSON object for crawl_config")
17
+ parser.add_argument("--limit", type=int, default=None, help="Maximum pages to crawl (backend max: 5000)")
18
+ parser.add_argument("--max-depth", type=int, default=None, help="Maximum crawl depth (backend max: 10)")
19
+ parser.add_argument(
20
+ "--include-path",
21
+ action="append",
22
+ dest="include_paths",
23
+ default=None,
24
+ help="Clean path pattern to include; repeatable. Supports * wildcards.",
25
+ )
26
+ parser.add_argument(
27
+ "--exclude-path",
28
+ action="append",
29
+ dest="exclude_paths",
30
+ default=None,
31
+ help="Clean path pattern to exclude; repeatable. Supports * wildcards.",
32
+ )
33
+ parser.add_argument("--allow-subdomains", action="store_true", default=None,
34
+ help="Allow crawling subdomains of the start URL host")
35
+ parser.add_argument("--allow-external-links", action="store_true", default=None,
36
+ help="Allow crawling links outside the start URL host")
37
+ parser.add_argument("--ignore-query-parameters", action="store_true", dest="ignore_query_parameters", default=None,
38
+ help="Treat URLs that differ only by query string as the same page")
39
+ parser.add_argument("--use-query-parameters", action="store_false", dest="ignore_query_parameters",
40
+ help="Treat URLs with different query strings as distinct pages")
41
+ parser.add_argument("--ignore-sitemap", action="store_true", dest="ignore_sitemap", default=None,
42
+ help="Skip sitemap discovery")
43
+ parser.add_argument("--use-sitemap", action="store_false", dest="ignore_sitemap",
44
+ help="Allow sitemap discovery")
45
+ parser.add_argument("--only-main-content", action="store_true", dest="only_main_content", default=None,
46
+ help="Extract only the main content area")
47
+ parser.add_argument("--include-page-chrome", action="store_false", dest="only_main_content",
48
+ help="Keep page navigation, footer, and other chrome")
49
+
50
+
14
51
  def register(subparsers):
15
52
  k = subparsers.add_parser("kb", help="Knowledge base operations")
16
53
  sub = k.add_subparsers(dest="action", required=True)
@@ -70,6 +107,8 @@ def register(subparsers):
70
107
  p.add_argument("--context-object-id", type=int, required=True,
71
108
  help="KB file snapshot_object_id from `codeer kb files`")
72
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")
73
112
  p.add_argument("--dry-run", action="store_true",
74
113
  help="Print intended request without writing server state.")
75
114
  p.add_argument("--out", default=None, help="Write result JSON to this file too")
@@ -80,6 +119,8 @@ def register(subparsers):
80
119
  p.add_argument("--context-object-id", type=int, default=None,
81
120
  help="Move FAQ to a different KB file snapshot_object_id")
82
121
  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")
83
124
  p.add_argument("--dry-run", action="store_true",
84
125
  help="Print intended request without writing server state.")
85
126
  p.add_argument("--out", default=None, help="Write result JSON to this file too")
@@ -92,6 +133,51 @@ def register(subparsers):
92
133
  p.add_argument("--out", default=None, help="Write result JSON to this file too")
93
134
  p.set_defaults(func=run_faq_delete)
94
135
 
136
+ p = sub.add_parser("crawl-create", help="Create a website-crawler KB folder; run --dry-run first")
137
+ p.add_argument("--url", required=True, help="Starting URL to crawl")
138
+ p.add_argument("--folder-name", default=None, help="KB folder name")
139
+ _add_crawl_config_args(p)
140
+ p.add_argument("--dry-run", action="store_true",
141
+ help="Print intended request without writing server state.")
142
+ p.add_argument("--out", default=None, help="Write result JSON to this file too")
143
+ p.set_defaults(func=run_crawl_create)
144
+
145
+ p = sub.add_parser("crawl-update", help="Update a website crawl target; run --dry-run first")
146
+ p.add_argument("--target-id", type=int, required=True)
147
+ p.add_argument("--url", required=True, help="Updated starting URL")
148
+ _add_crawl_config_args(p)
149
+ p.add_argument("--dry-run", action="store_true",
150
+ help="Print intended request without writing server state.")
151
+ p.add_argument("--out", default=None, help="Write result JSON to this file too")
152
+ p.set_defaults(func=run_crawl_update)
153
+
154
+ p = sub.add_parser("crawl-state", help="Read website crawl state for a crawler folder")
155
+ p.add_argument("--folder-id", required=True, help="KnowledgeNode folder UUID")
156
+ p.add_argument("--out", default=None, help="Write result JSON to this file too")
157
+ p.set_defaults(func=run_crawl_state)
158
+
159
+ p = sub.add_parser("crawl-sync", help="Start a website crawl sync job; run --dry-run first")
160
+ p.add_argument("--target-id", type=int, required=True)
161
+ p.add_argument("--dry-run", action="store_true",
162
+ help="Print intended request without writing server state.")
163
+ p.add_argument("--out", default=None, help="Write result JSON to this file too")
164
+ p.set_defaults(func=run_crawl_sync)
165
+
166
+ p = sub.add_parser("crawl-cancel", help="Cancel the active website crawl job; run --dry-run first")
167
+ p.add_argument("--target-id", type=int, required=True)
168
+ p.add_argument("--dry-run", action="store_true",
169
+ help="Print intended request without writing server state.")
170
+ p.add_argument("--out", default=None, help="Write result JSON to this file too")
171
+ p.set_defaults(func=run_crawl_cancel)
172
+
173
+ p = sub.add_parser("crawl-failures", help="List failed pages for a website crawl job")
174
+ p.add_argument("--job-id", type=int, required=True)
175
+ p.add_argument("--status", default="DOWNLOAD_FAILED,FAILED")
176
+ p.add_argument("--limit", type=int, default=50)
177
+ p.add_argument("--offset", type=int, default=0)
178
+ p.add_argument("--out", default=None, help="Write result JSON to this file too")
179
+ p.set_defaults(func=run_crawl_failures)
180
+
95
181
 
96
182
  def _node_summary(node: dict) -> dict:
97
183
  return {
@@ -114,6 +200,7 @@ def _faq_summary(faq: dict) -> dict:
114
200
  "id": faq.get("id"),
115
201
  "context_object_id": faq.get("context_object_id"),
116
202
  "question": faq.get("question"),
203
+ "ranges": faq.get("ranges") or [],
117
204
  "has_question_embedding": faq.get("has_question_embedding"),
118
205
  "updated_at": faq.get("updated_at"),
119
206
  }
@@ -130,6 +217,54 @@ def _dry_run(path: str | None, result: dict) -> int:
130
217
  return 0
131
218
 
132
219
 
220
+ def _parse_faq_range(value: str) -> dict[str, int]:
221
+ try:
222
+ start_raw, end_raw = value.split(":", 1)
223
+ start_line = int(start_raw)
224
+ end_line = int(end_raw)
225
+ except ValueError as exc:
226
+ raise argparse.ArgumentTypeError("expected START_LINE:END_LINE") from exc
227
+ if start_line < 1 or end_line < 1:
228
+ 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}
232
+
233
+
234
+ def _parse_config_json(config_json: str | None) -> dict | None:
235
+ if not config_json:
236
+ return None
237
+ value = json.loads(config_json)
238
+ if not isinstance(value, dict):
239
+ raise SystemExit("--config-json must decode to a JSON object")
240
+ return value
241
+
242
+
243
+ def _crawl_config_from_args(args) -> dict | None:
244
+ config = _parse_config_json(args.config_json)
245
+ has_config = config is not None
246
+ if config is None:
247
+ config = {}
248
+
249
+ for key, attr in (
250
+ ("limit", "limit"),
251
+ ("maxDepth", "max_depth"),
252
+ ("includePaths", "include_paths"),
253
+ ("excludePaths", "exclude_paths"),
254
+ ("allowSubdomains", "allow_subdomains"),
255
+ ("allowExternalLinks", "allow_external_links"),
256
+ ("ignoreQueryParameters", "ignore_query_parameters"),
257
+ ("ignoreSitemap", "ignore_sitemap"),
258
+ ("onlyMainContent", "only_main_content"),
259
+ ):
260
+ value = getattr(args, attr)
261
+ if value is not None:
262
+ config[key] = value
263
+ has_config = True
264
+
265
+ return config if has_config else None
266
+
267
+
133
268
  def run_list(args, client) -> int:
134
269
  workspace_id, organization_id = client.resolve_scope()
135
270
  nodes = kb_mod.list_nodes(
@@ -269,6 +404,8 @@ def run_faq_get(args, client) -> int:
269
404
 
270
405
  def run_faq_create(args, client) -> int:
271
406
  body = {"context_object_id": args.context_object_id, "question": args.question}
407
+ if args.ranges is not None:
408
+ body["ranges"] = args.ranges
272
409
  if args.dry_run:
273
410
  return _dry_run(
274
411
  args.out,
@@ -288,6 +425,7 @@ def run_faq_create(args, client) -> int:
288
425
  client,
289
426
  context_object_id=args.context_object_id,
290
427
  question=args.question,
428
+ ranges=args.ranges,
291
429
  )
292
430
  )
293
431
  _print_and_write(args.out, faq)
@@ -295,8 +433,8 @@ def run_faq_create(args, client) -> int:
295
433
 
296
434
 
297
435
  def run_faq_update(args, client) -> int:
298
- if args.context_object_id is None and args.question is None:
299
- log("error: provide --context-object-id or --question")
436
+ if args.context_object_id is None and args.question is None and args.ranges is None:
437
+ log("error: provide --context-object-id, --question, or --range")
300
438
  return 2
301
439
 
302
440
  body: dict[str, object] = {}
@@ -304,6 +442,8 @@ def run_faq_update(args, client) -> int:
304
442
  body["context_object_id"] = args.context_object_id
305
443
  if args.question is not None:
306
444
  body["question"] = args.question
445
+ if args.ranges is not None:
446
+ body["ranges"] = args.ranges
307
447
 
308
448
  if args.dry_run:
309
449
  return _dry_run(
@@ -325,6 +465,7 @@ def run_faq_update(args, client) -> int:
325
465
  faq_id=args.faq_id,
326
466
  context_object_id=args.context_object_id,
327
467
  question=args.question,
468
+ ranges=args.ranges,
328
469
  )
329
470
  )
330
471
  _print_and_write(args.out, faq)
@@ -348,3 +489,127 @@ def run_faq_delete(args, client) -> int:
348
489
  response = strip_noisy_fields(kb_mod.delete_context_obj_faq(client, faq_id=args.faq_id))
349
490
  _print_and_write(args.out, response)
350
491
  return 0
492
+
493
+
494
+ def run_crawl_create(args, client) -> int:
495
+ crawl_config = _crawl_config_from_args(args)
496
+ body: dict[str, object] = {"start_url": args.url}
497
+ if args.folder_name is not None:
498
+ body["folder_name"] = args.folder_name
499
+ if crawl_config is not None:
500
+ body["crawl_config"] = crawl_config
501
+
502
+ if args.dry_run:
503
+ return _dry_run(
504
+ args.out,
505
+ {
506
+ "dry_run": True,
507
+ "operation": "kb_crawl_create",
508
+ "method": "POST",
509
+ "path": "/external/knowledge-bases/website-crawls",
510
+ "body": body,
511
+ "would_write_server_state": True,
512
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
513
+ },
514
+ )
515
+
516
+ response = strip_noisy_fields(
517
+ kb_mod.create_website_crawl(
518
+ client,
519
+ start_url=args.url,
520
+ folder_name=args.folder_name,
521
+ crawl_config=crawl_config,
522
+ )
523
+ )
524
+ _print_and_write(args.out, response)
525
+ return 0
526
+
527
+
528
+ def run_crawl_update(args, client) -> int:
529
+ crawl_config = _crawl_config_from_args(args)
530
+ body: dict[str, object] = {"start_url": args.url}
531
+ if crawl_config is not None:
532
+ body["crawl_config"] = crawl_config
533
+
534
+ if args.dry_run:
535
+ return _dry_run(
536
+ args.out,
537
+ {
538
+ "dry_run": True,
539
+ "operation": "kb_crawl_update",
540
+ "method": "PATCH",
541
+ "path": f"/external/knowledge-bases/website-crawls/{args.target_id}",
542
+ "body": body,
543
+ "would_write_server_state": True,
544
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
545
+ },
546
+ )
547
+
548
+ response = strip_noisy_fields(
549
+ kb_mod.update_website_crawl(
550
+ client,
551
+ target_id=args.target_id,
552
+ start_url=args.url,
553
+ crawl_config=crawl_config,
554
+ )
555
+ )
556
+ _print_and_write(args.out, response)
557
+ return 0
558
+
559
+
560
+ def run_crawl_state(args, client) -> int:
561
+ response = strip_noisy_fields(kb_mod.get_website_crawl_state(client, folder_id=args.folder_id))
562
+ _print_and_write(args.out, response)
563
+ return 0
564
+
565
+
566
+ def run_crawl_sync(args, client) -> int:
567
+ if args.dry_run:
568
+ return _dry_run(
569
+ args.out,
570
+ {
571
+ "dry_run": True,
572
+ "operation": "kb_crawl_sync",
573
+ "method": "POST",
574
+ "path": f"/external/knowledge-bases/website-crawls/{args.target_id}:sync",
575
+ "would_write_server_state": True,
576
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
577
+ },
578
+ )
579
+
580
+ response = strip_noisy_fields(kb_mod.sync_website_crawl(client, target_id=args.target_id))
581
+ _print_and_write(args.out, response)
582
+ return 0
583
+
584
+
585
+ def run_crawl_cancel(args, client) -> int:
586
+ if args.dry_run:
587
+ return _dry_run(
588
+ args.out,
589
+ {
590
+ "dry_run": True,
591
+ "operation": "kb_crawl_cancel",
592
+ "method": "POST",
593
+ "path": f"/external/knowledge-bases/website-crawls/{args.target_id}:cancel",
594
+ "would_write_server_state": True,
595
+ "next_step": "Review this summary, then rerun without --dry-run after approval.",
596
+ },
597
+ )
598
+
599
+ response = strip_noisy_fields(kb_mod.cancel_website_crawl(client, target_id=args.target_id))
600
+ _print_and_write(args.out, response)
601
+ return 0
602
+
603
+
604
+ def run_crawl_failures(args, client) -> int:
605
+ response = strip_noisy_fields(
606
+ kb_mod.get_website_crawl_failures(
607
+ client,
608
+ job_id=args.job_id,
609
+ status=args.status,
610
+ limit=args.limit,
611
+ offset=args.offset,
612
+ )
613
+ )
614
+ _print_and_write(args.out, response)
615
+ return 0
@@ -124,7 +124,7 @@ def run_add(args, client=None) -> int:
124
124
  data["default"] = args.name
125
125
  _write_profiles(data)
126
126
  print(f"Saved profile {args.name}")
127
- return 0
127
+ return run_use(args)
128
128
 
129
129
 
130
130
  def run_use(args, client=None) -> int:
codeer_cli/constants.py CHANGED
@@ -19,6 +19,7 @@ UNIFIED_TOOL_TYPES: frozenset[str] = frozenset({
19
19
  "payment",
20
20
  "memory",
21
21
  "http_request",
22
+ "text2speech",
22
23
  })
23
24
 
24
25
  # Valid type values for a field inside ``custom_form_schema.fields[]``.
codeer_cli/kb.py CHANGED
@@ -36,7 +36,7 @@ def _guess_mime(filename: str) -> str:
36
36
  return guess or "application/octet-stream"
37
37
 
38
38
 
39
- def _base(organization_id: str, workspace_id: str) -> str:
39
+ def _base(organization_id: str = "", workspace_id: str = "") -> str:
40
40
  return "/external/knowledge-bases"
41
41
 
42
42
 
@@ -252,11 +252,12 @@ def create_context_obj_faq(
252
252
  *,
253
253
  context_object_id: int,
254
254
  question: str,
255
+ ranges: Optional[list[dict[str, int]]] = None,
255
256
  ) -> dict:
256
- return client.post(
257
- _faq_base(),
258
- json={"context_object_id": context_object_id, "question": question},
259
- )
257
+ body: dict[str, Any] = {"context_object_id": context_object_id, "question": question}
258
+ if ranges is not None:
259
+ body["ranges"] = ranges
260
+ return client.post(_faq_base(), json=body)
260
261
 
261
262
 
262
263
  def update_context_obj_faq(
@@ -265,14 +266,71 @@ def update_context_obj_faq(
265
266
  faq_id: int,
266
267
  context_object_id: Optional[int] = None,
267
268
  question: Optional[str] = None,
269
+ ranges: Optional[list[dict[str, int]]] = None,
268
270
  ) -> dict:
269
271
  body: dict[str, Any] = {}
270
272
  if context_object_id is not None:
271
273
  body["context_object_id"] = context_object_id
272
274
  if question is not None:
273
275
  body["question"] = question
276
+ if ranges is not None:
277
+ body["ranges"] = ranges
274
278
  return client.patch(f"{_faq_base()}/{faq_id}", json=body)
275
279
 
276
280
 
277
281
  def delete_context_obj_faq(client: CodeerClient, *, faq_id: int) -> dict:
278
282
  return client.delete(f"{_faq_base()}/{faq_id}")
283
+
284
+
285
+ def create_website_crawl(
286
+ client: CodeerClient,
287
+ *,
288
+ start_url: str,
289
+ folder_name: Optional[str] = None,
290
+ crawl_config: Optional[dict[str, Any]] = None,
291
+ ) -> dict:
292
+ body: dict[str, Any] = {"start_url": start_url}
293
+ if folder_name is not None:
294
+ body["folder_name"] = folder_name
295
+ if crawl_config is not None:
296
+ body["crawl_config"] = crawl_config
297
+ return client.post(f"{_base()}/website-crawls", json=body)
298
+
299
+
300
+ def update_website_crawl(
301
+ client: CodeerClient,
302
+ *,
303
+ target_id: int,
304
+ start_url: str,
305
+ crawl_config: Optional[dict[str, Any]] = None,
306
+ ) -> dict:
307
+ body: dict[str, Any] = {"start_url": start_url}
308
+ if crawl_config is not None:
309
+ body["crawl_config"] = crawl_config
310
+ return client.patch(f"{_base()}/website-crawls/{target_id}", json=body)
311
+
312
+
313
+ def sync_website_crawl(client: CodeerClient, *, target_id: int) -> dict:
314
+ return client.post(f"{_base()}/website-crawls/{target_id}:sync", json={})
315
+
316
+
317
+ def cancel_website_crawl(client: CodeerClient, *, target_id: int) -> dict:
318
+ return client.post(f"{_base()}/website-crawls/{target_id}:cancel", json={})
319
+
320
+
321
+ def get_website_crawl_state(client: CodeerClient, *, folder_id: str) -> dict:
322
+ return client.get(f"{_base()}/nodes/{folder_id}/website-crawl-state")
323
+
324
+
325
+ def get_website_crawl_failures(
326
+ client: CodeerClient,
327
+ *,
328
+ job_id: int,
329
+ status: str = "DOWNLOAD_FAILED,FAILED",
330
+ limit: int = 50,
331
+ offset: int = 0,
332
+ ) -> dict:
333
+ return client.get(
334
+ f"{_base()}/website-crawl-jobs/{job_id}/failures",
335
+ params={"status": status, "limit": limit, "offset": offset},
336
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeer-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
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
@@ -22,25 +22,35 @@ Standalone CLI for managing Codeer agents over the Codeer API.
22
22
 
23
23
  ## User install
24
24
 
25
- After the package is published to PyPI, install the CLI as an isolated command
26
- line tool:
25
+ Install the CLI from PyPI with `pipx`:
27
26
 
28
27
  ```bash
29
- uv tool install codeer-cli
28
+ pipx install codeer-cli
30
29
  ```
31
30
 
32
- Until the package is published, install directly from this repository:
31
+ Verify that the command is available:
33
32
 
34
33
  ```bash
35
- uv tool install 'git+https://github.com/<org>/codeer-skills.git#subdirectory=codeer-cli'
34
+ codeer --help
36
35
  ```
37
36
 
38
- Replace `<org>` with the GitHub organization or user that hosts this repository.
37
+ If `pipx` is not installed:
39
38
 
40
- Verify that the command is available:
39
+ ```bash
40
+ python -m pip install --user pipx
41
+ python -m pipx ensurepath
42
+ ```
43
+
44
+ Then restart the terminal and run:
41
45
 
42
46
  ```bash
43
- codeer --help
47
+ pipx install codeer-cli
48
+ ```
49
+
50
+ As a fallback, you can install into your user Python environment:
51
+
52
+ ```bash
53
+ python -m pip install --user codeer-cli
44
54
  ```
45
55
 
46
56
  ## Credentials
@@ -107,6 +117,21 @@ Validate setup before API work:
107
117
  codeer check
108
118
  ```
109
119
 
120
+ ## Upgrade and uninstall
121
+
122
+ Upgrade the CLI:
123
+
124
+ ```bash
125
+ pipx upgrade codeer-cli
126
+ codeer check
127
+ ```
128
+
129
+ Remove the CLI:
130
+
131
+ ```bash
132
+ pipx uninstall codeer-cli
133
+ ```
134
+
110
135
  ## Output policy for coding agents
111
136
 
112
137
  The CLI is optimized for Codex, Claude Code, Claude Cowork, and similar coding
@@ -133,16 +158,40 @@ Avoid piping large raw JSON directly into agent chat. Prefer `--out`, then ask
133
158
  the coding agent to inspect targeted summaries, IDs, failing cases, or selected
134
159
  snippets from the saved file.
135
160
 
161
+ ## Website crawler KBs
162
+
163
+ Website-backed KB folders can be created and updated with `codeer kb crawl-*`.
164
+ Always preview crawler mutations with `--dry-run` first:
165
+
166
+ ```bash
167
+ codeer kb crawl-create \
168
+ --url https://example.com/docs \
169
+ --folder-name "Product Docs" \
170
+ --include-path "/docs*" \
171
+ --exclude-path "/docs/private*" \
172
+ --limit 250 \
173
+ --max-depth 3 \
174
+ --only-main-content \
175
+ --dry-run
176
+ ```
177
+
178
+ `--include-path` and `--exclude-path` are repeatable clean path patterns. Quote
179
+ paths containing `*` so the shell passes the wildcard to the CLI. Advanced
180
+ settings can still be passed through `--config-json`; explicit crawler flags
181
+ override matching JSON keys.
182
+
136
183
  ## Context Object FAQ
137
184
 
138
185
  Use Context Object FAQ entries to route high-value questions to a canonical KB
139
186
  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`.
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.
141
190
 
142
191
  ```bash
143
192
  codeer kb files --kb-id <kb-id>
144
193
  codeer kb faq-list --context-object-id <snapshot-object-id>
145
- codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --dry-run
194
+ codeer kb faq-create --context-object-id <snapshot-object-id> --question "..." --range 12:18 --dry-run
146
195
  ```
147
196
 
148
197
  After reviewing the dry-run output, rerun the create/update/delete command
@@ -1,23 +1,23 @@
1
1
  codeer_cli/__init__.py,sha256=-0gL8upoSsLAnXAfcRrwqZYJbwG0knzQoFf94O7Nc7c,1817
2
2
  codeer_cli/_validate.py,sha256=pKUJa2TyTpERx5xmiYNZRn7tFqDxLZ2fF1rHAf1oz14,5415
3
- codeer_cli/agents.py,sha256=a1w1WKrxY9jWJNSNT9PONRh9ZA7vEjrYlzdYkALhIC0,5361
3
+ codeer_cli/agents.py,sha256=diodgiGhXlowEi8sbCzcSK1qSeCLF2fBe6QBs3Sq_x8,5617
4
4
  codeer_cli/chats.py,sha256=YVrZJhoa-d67o6tzX6riGXsbA-ehyhOxrZ8zRCcJNro,2675
5
5
  codeer_cli/cli.py,sha256=kaXTCBfzq64fLJDqKH39cN0ds7qv-F-eRxzUsGmCvv0,3901
6
- codeer_cli/client.py,sha256=-FddTZaNPAsVDV4wdgHvdHUIiPyUwgiLCul0-IyJ_-c,9860
7
- codeer_cli/constants.py,sha256=YthRM77jTh9npRDES0h02AeOKhx6fwAqPQwUjXJxhMY,2304
6
+ codeer_cli/client.py,sha256=LpHVqf1IYNg1wFfIHnO9q4xg2h3IiGOitzCnvwB-Bcw,9809
7
+ codeer_cli/constants.py,sha256=D1pV3wCoqYybrKGKeoupYjjFWLfaFviKp1yL7oh6Qso,2323
8
8
  codeer_cli/eval_.py,sha256=EsH8f8nT8x9MFlUhpHWSAU4aNPkNceD-Tw5GM15Ae1I,14157
9
9
  codeer_cli/histories.py,sha256=tk28git_peX4x703CIDU8u72JtlGaytyrtlHfxlK-7A,5979
10
- codeer_cli/kb.py,sha256=_kEQHT0CCDaD_988LIxxCtW94MjXObEND5GBWbU3bvM,8560
10
+ codeer_cli/kb.py,sha256=--0MvZJ2OsIbzLh-4TQ-wR7dVK42eaXw-L0qDGFOaxg,10417
11
11
  codeer_cli/parse.py,sha256=qrjZn0MUTjGfucp4cwxy8Pt7WS-0x15kK5F7kWTY8Ps,21818
12
12
  codeer_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  codeer_cli/commands/_util.py,sha256=VOB_HMWYzHFNY1ElLOED1HB6fpFpsniqH6Yx3VUlMrY,1644
14
- codeer_cli/commands/agent.py,sha256=wugAVSU0aGniMau43ZzCvWYAQsAxETH5YqKj4TaRGV8,11922
14
+ codeer_cli/commands/agent.py,sha256=amvfVVrbPOkbYKCGvA6EJB30C-aY6WSdfR7u7FklXbs,14793
15
15
  codeer_cli/commands/check.py,sha256=lTxolx1mIJ8jldPhJ5FXqie9nbCLVOO-sDPOHTSy1-w,3817
16
16
  codeer_cli/commands/eval_cmd.py,sha256=yvqjPXMOE7V0Hv53iEW5HvIo610dgiIG4BpKGWzZY4I,46965
17
17
  codeer_cli/commands/history.py,sha256=Jv7t0GhSZcbZ8OuIXZT34CixXt7ECEVP3nZ-WW_Ya9E,12026
18
- codeer_cli/commands/kb.py,sha256=cLaW5Exdrtgm0jTkyWMR3ajxfG2E7u5AXU1eGIWUBEk,13522
19
- codeer_cli/commands/profile.py,sha256=aOc9S4HvyFyQ3vH4yK_BcF4yd7VZd2kVnKyOgtqLP_I,6509
20
- codeer_cli-0.1.1.dist-info/METADATA,sha256=dIE24TYRemJy4XpAv13UqQuRRY2j27MQAmix8MQSwPM,4342
21
- codeer_cli-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
22
- codeer_cli-0.1.1.dist-info/entry_points.txt,sha256=-nXIrlm5SR5r7gg3y8AS0tN66MwmvNHsrlwLNQNGD50,47
23
- codeer_cli-0.1.1.dist-info/RECORD,,
18
+ codeer_cli/commands/kb.py,sha256=vXwgiGHYrLywm3O41-D__7m3d1uZUcw3YYuxKH-08i0,24528
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any