recala-cli 0.6.0__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.
Files changed (67) hide show
  1. recala_cli/__init__.py +3 -0
  2. recala_cli/__main__.py +3 -0
  3. recala_cli/article_runs.py +372 -0
  4. recala_cli/auth.py +196 -0
  5. recala_cli/client.py +103 -0
  6. recala_cli/commands/__init__.py +1 -0
  7. recala_cli/commands/article.py +835 -0
  8. recala_cli/commands/auth.py +207 -0
  9. recala_cli/commands/billing.py +76 -0
  10. recala_cli/commands/bing.py +164 -0
  11. recala_cli/commands/cadence.py +217 -0
  12. recala_cli/commands/connect.py +395 -0
  13. recala_cli/commands/credits.py +46 -0
  14. recala_cli/commands/faithful_translations.py +70 -0
  15. recala_cli/commands/gsc.py +79 -0
  16. recala_cli/commands/index.py +293 -0
  17. recala_cli/commands/keys.py +98 -0
  18. recala_cli/commands/knowledge.py +245 -0
  19. recala_cli/commands/languages.py +119 -0
  20. recala_cli/commands/monthly_report.py +152 -0
  21. recala_cli/commands/notifications.py +106 -0
  22. recala_cli/commands/onboard.py +237 -0
  23. recala_cli/commands/org.py +352 -0
  24. recala_cli/commands/profile.py +70 -0
  25. recala_cli/commands/questions.py +246 -0
  26. recala_cli/commands/reads.py +320 -0
  27. recala_cli/commands/refresh.py +138 -0
  28. recala_cli/commands/refusals.py +20 -0
  29. recala_cli/commands/schema.py +47 -0
  30. recala_cli/commands/shell.py +81 -0
  31. recala_cli/commands/skills.py +92 -0
  32. recala_cli/commands/slot_topic.py +94 -0
  33. recala_cli/commands/status.py +153 -0
  34. recala_cli/commands/topics.py +328 -0
  35. recala_cli/config.py +176 -0
  36. recala_cli/confirm.py +137 -0
  37. recala_cli/display.py +40 -0
  38. recala_cli/endpoints.py +258 -0
  39. recala_cli/errors.py +220 -0
  40. recala_cli/human.py +324 -0
  41. recala_cli/ids.py +139 -0
  42. recala_cli/labels.py +74 -0
  43. recala_cli/main.py +129 -0
  44. recala_cli/onboarding.py +522 -0
  45. recala_cli/organizations.py +197 -0
  46. recala_cli/output.py +34 -0
  47. recala_cli/parallel.py +18 -0
  48. recala_cli/polling.py +102 -0
  49. recala_cli/prompts.py +88 -0
  50. recala_cli/runtime.py +216 -0
  51. recala_cli/search_numbers.py +22 -0
  52. recala_cli/skills/recala-apply-revision-notes/SKILL.md +39 -0
  53. recala_cli/skills/recala-article-signoff/SKILL.md +67 -0
  54. recala_cli/skills/recala-cli-contract.md +78 -0
  55. recala_cli/skills/recala-connect-search-console/SKILL.md +29 -0
  56. recala_cli/skills/recala-onboard/SKILL.md +25 -0
  57. recala_cli/skills/recala-onboard-brand/SKILL.md +25 -0
  58. recala_cli/skills/recala-plan-month/SKILL.md +33 -0
  59. recala_cli/skills/recala-schedule-approved/SKILL.md +36 -0
  60. recala_cli/skills/recala-weekly-authority-report/SKILL.md +35 -0
  61. recala_cli/ui.py +260 -0
  62. recala_cli/updates.py +43 -0
  63. recala_cli/views.py +372 -0
  64. recala_cli-0.6.0.dist-info/METADATA +63 -0
  65. recala_cli-0.6.0.dist-info/RECORD +67 -0
  66. recala_cli-0.6.0.dist-info/WHEEL +4 -0
  67. recala_cli-0.6.0.dist-info/entry_points.txt +2 -0
recala_cli/config.py ADDED
@@ -0,0 +1,176 @@
1
+ """Where the CLI keeps its settings and credentials, and the order it reads them in.
2
+
3
+ Files in one directory (RECALA_CONFIG_DIR, else $XDG_CONFIG_HOME/recala,
4
+ else ~/.config/recala), all written 0600:
5
+
6
+ * ``config.json``: the API URL, the web URL and the active organization. Not secret.
7
+ * ``credentials.json``: the stored session or key, never printed.
8
+ * ``grants.json``: the person's [a]ll answer for the current sign-in (see ``confirm``).
9
+ * ``onboarding.json``: which onboarding steps this machine finished, per organization.
10
+
11
+ Precedence, most specific first: a flag, then an environment variable, then the
12
+ file. Agents and CI set RECALA_API_KEY and RECALA_ORG and never touch the files.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import tempfile
20
+ import time
21
+ from pathlib import Path
22
+ from typing import Any
23
+ from uuid import UUID
24
+
25
+ from recala_cli.errors import CliError, ExitCode
26
+
27
+ DEFAULT_API_URL = "https://recala-production.up.railway.app"
28
+ DEFAULT_WEB_URL = "https://recala.co"
29
+ _CONFIG = "config.json"
30
+ _CREDENTIALS = "credentials.json"
31
+ _GRANTS = "grants.json"
32
+ _ONBOARDING = "onboarding.json"
33
+ _SLUGS = "org-slugs.json"
34
+ _SLUG_TTL_SECONDS = 24 * 60 * 60
35
+
36
+
37
+ def config_dir() -> Path:
38
+ explicit = os.environ.get("RECALA_CONFIG_DIR")
39
+ if explicit:
40
+ return Path(explicit)
41
+ base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
42
+ return Path(base) / "recala"
43
+
44
+
45
+ def api_url(flag: str | None = None) -> str:
46
+ return (flag or os.environ.get("RECALA_API_URL") or _read(_CONFIG).get("api_url") or DEFAULT_API_URL).rstrip("/")
47
+
48
+
49
+ def web_url() -> str:
50
+ """Where the person's browser goes (sign-in, Search Console, checkout): RECALA_WEB_URL, else what login learned."""
51
+ return (os.environ.get("RECALA_WEB_URL") or _read(_CONFIG).get("web_url") or DEFAULT_WEB_URL).rstrip("/")
52
+
53
+
54
+ def set_web_url(url: str) -> None:
55
+ settings = _read(_CONFIG)
56
+ settings["web_url"] = url.rstrip("/")
57
+ _write(_CONFIG, settings)
58
+
59
+
60
+ def active_org(flag: str | None = None) -> str | None:
61
+ selector = flag if flag is not None else os.environ.get("RECALA_ORG")
62
+ if selector is not None:
63
+ if not selector.strip():
64
+ raise CliError(ExitCode.USAGE, "invalid_org", "--org needs a company name, slug or id.")
65
+ return selector
66
+ return _read(_CONFIG).get("org_id")
67
+
68
+
69
+ def saved_active_org() -> str | None:
70
+ """The company saved by `recala company use`, ignoring --org and RECALA_ORG."""
71
+ return _read(_CONFIG).get("org_id")
72
+
73
+
74
+ def set_active_org(org_id: str) -> None:
75
+ settings = _read(_CONFIG)
76
+ settings["org_id"] = org_id
77
+ _write(_CONFIG, settings)
78
+
79
+
80
+ def is_org_id(value: str) -> bool:
81
+ try:
82
+ UUID(value)
83
+ except ValueError:
84
+ return False
85
+ return True
86
+
87
+
88
+ def environment_token() -> str | None:
89
+ return os.environ.get("RECALA_API_KEY") or os.environ.get("RECALA_TOKEN")
90
+
91
+
92
+ def load_credentials() -> dict[str, Any]:
93
+ return _read(_CREDENTIALS)
94
+
95
+
96
+ def save_credentials(credentials: dict[str, Any]) -> None:
97
+ _write(_CREDENTIALS, credentials)
98
+
99
+
100
+ def forget_credentials() -> None:
101
+ (config_dir() / _CREDENTIALS).unlink(missing_ok=True)
102
+ forget_grant()
103
+
104
+
105
+ def load_grant() -> dict[str, Any]:
106
+ return _read(_GRANTS)
107
+
108
+
109
+ def save_grant(grant: dict[str, Any]) -> None:
110
+ _write(_GRANTS, grant)
111
+
112
+
113
+ def forget_grant() -> None:
114
+ (config_dir() / _GRANTS).unlink(missing_ok=True)
115
+
116
+
117
+ def onboarding_progress(org_id: str) -> dict[str, Any]:
118
+ entry = _read(_ONBOARDING).get(org_id)
119
+ return entry if isinstance(entry, dict) else {}
120
+
121
+
122
+ def save_onboarding_progress(org_id: str, progress: dict[str, Any]) -> None:
123
+ everything = _read(_ONBOARDING)
124
+ everything[org_id] = progress
125
+ _write(_ONBOARDING, everything)
126
+
127
+
128
+ def cached_org_slug(org_id: str) -> str | None:
129
+ """A company's slug read in the last day: /me omits slugs, and reading each company costs a round trip."""
130
+ entry = _read_quietly(_SLUGS).get(org_id)
131
+ fresh = isinstance(entry, dict) and time.time() - float(entry.get("at") or 0) < _SLUG_TTL_SECONDS
132
+ return entry.get("slug") if fresh and isinstance(entry.get("slug"), str) else None
133
+
134
+
135
+ def remember_org_slugs(identities: dict[str, dict[str, Any]]) -> None:
136
+ learned = {org_id: {"slug": found["slug"], "at": time.time()}
137
+ for org_id, found in identities.items() if isinstance(found.get("slug"), str)}
138
+ if not learned:
139
+ return
140
+ try:
141
+ _write(_SLUGS, {**_read_quietly(_SLUGS), **learned})
142
+ except OSError:
143
+ pass # a cache that cannot be written only costs the next command a read
144
+
145
+
146
+ def _read_quietly(name: str) -> dict[str, Any]:
147
+ try:
148
+ return _read(name)
149
+ except (OSError, ValueError):
150
+ return {}
151
+
152
+
153
+ def _read(name: str) -> dict[str, Any]:
154
+ path = config_dir() / name
155
+ if not path.exists():
156
+ return {}
157
+ data = json.loads(path.read_text())
158
+ return data if isinstance(data, dict) else {}
159
+
160
+
161
+ def _write(name: str, data: dict[str, Any]) -> None:
162
+ """Atomic and owner-only: a crash never leaves half a file, and no other user can read it.
163
+
164
+ ``mkstemp`` creates the file 0600 (a umask can only narrow that), and ``os.replace``
165
+ swaps it in whole, so even a credentials file some other tool left 0644 comes back 0600.
166
+ """
167
+ directory = config_dir()
168
+ directory.mkdir(parents=True, exist_ok=True, mode=0o700)
169
+ handle, temporary = tempfile.mkstemp(dir=directory, prefix=f".{name}.")
170
+ try:
171
+ with os.fdopen(handle, "w") as stream:
172
+ json.dump(data, stream)
173
+ os.replace(temporary, directory / name)
174
+ except BaseException:
175
+ Path(temporary).unlink(missing_ok=True)
176
+ raise
recala_cli/confirm.py ADDED
@@ -0,0 +1,137 @@
1
+ """Every write asks the person first: [y]es / [n]o / [a]ll (founder decision, 5 Oct 2026).
2
+
3
+ In a terminal the CLI asks before each write. ``a`` confirms the rest of this
4
+ command and records a grant for this sign-in (``grants.json``, bound to the
5
+ session's local id, 12 hours at most, gone at logout or the next login).
6
+
7
+ Without a terminal (an agent, a pipe, --json) nothing ever prompts. A write then
8
+ goes ahead only with:
9
+
10
+ * a prior [a]ll grant for this sign-in (``recala grant``, or an ``a`` answered
11
+ earlier), or
12
+ * an API key: its scopes are its authorization, checked by the server (spending
13
+ also needs the cost preview the server demands). An action that publishes
14
+ (approve, schedule, unschedule, unpublish, or a key that may do those) also
15
+ needs --yes, and --yes is accepted only from a key that holds ``publish``.
16
+
17
+ Otherwise nothing is sent and the command exits 10. This module only decides
18
+ whether to send; every request still meets the server's own gates.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import sys
24
+ import time
25
+ from datetime import datetime
26
+ from typing import Any
27
+
28
+ import click
29
+
30
+ from recala_cli import auth, config
31
+ from recala_cli.client import RecalaClient
32
+ from recala_cli.endpoints import ME
33
+ from recala_cli.errors import CliError, ExitCode
34
+ from recala_cli.output import notify
35
+
36
+ GRANT_HOURS = 12
37
+ PUBLISH_SCOPE = "publish"
38
+ _ANSWERS = {"y": "yes", "yes": "yes", "n": "no", "no": "no", "": "no", "a": "all", "all": "all"}
39
+ _ATTEMPTS = 3
40
+
41
+
42
+ def is_interactive(ctx: click.Context) -> bool:
43
+ """A person at a terminal: never under --json, and only when both stdin and stderr are a TTY."""
44
+ seams = ctx.find_root().obj or {}
45
+ if seams.get("as_json"):
46
+ return False
47
+ forced = seams.get("interactive")
48
+ if forced is not None:
49
+ return bool(forced)
50
+ return sys.stdin.isatty() and sys.stderr.isatty()
51
+
52
+
53
+ def grant_status() -> dict[str, Any]:
54
+ grant, session = config.load_grant(), auth.session_id()
55
+ active = bool(session) and grant.get("session_id") == session and time.time() < float(grant.get("expires_at") or 0)
56
+ return {"active": active, "expires_at": grant.get("expires_at") if active else None}
57
+
58
+
59
+ def require_current_signin() -> None:
60
+ if (not config.environment_token() and auth.credential_kind() in {"session", "access_token"}
61
+ and not auth.session_id()):
62
+ raise CliError(ExitCode.NOT_AUTHENTICATED, "legacy_signin",
63
+ "This sign-in predates saved confirmations. Run `recala login` again, then `recala grant`.")
64
+
65
+
66
+ def record_grant() -> dict[str, Any]:
67
+ require_current_signin()
68
+ session = auth.session_id()
69
+ if not session:
70
+ raise CliError(ExitCode.CONFLICT, "no_session",
71
+ "Only a person's sign-in (`recala login`) can hold an [a]ll grant; an API key uses its scopes.")
72
+ now = time.time()
73
+ config.save_grant({"session_id": session, "granted_at": now, "expires_at": now + GRANT_HOURS * 3600})
74
+ return grant_status()
75
+
76
+
77
+ def confirm_write(ctx: click.Context, client: RecalaClient | None, summary: str, *, publishes: bool = False,
78
+ yes: bool = False) -> None:
79
+ """Return when the person (or the key) confirmed this write; raise exit 10 when nothing may be sent."""
80
+ require_current_signin()
81
+ seams = ctx.find_root().obj
82
+ if seams.get("all_confirmed"):
83
+ return
84
+ with_key = auth.credential_kind() == "api_key"
85
+ if with_key and publishes and yes:
86
+ _require_publish_scope(seams, client)
87
+ return
88
+ grant = grant_status() if not with_key else {}
89
+ if grant.get("active"):
90
+ seams["all_confirmed"] = True
91
+ if not seams.get("as_json"):
92
+ until = datetime.fromtimestamp(float(grant["expires_at"])).strftime("%a %d %b, %H:%M")
93
+ notify(f"Confirmed by your agent grant (until {until}).")
94
+ return
95
+ if is_interactive(ctx):
96
+ _ask(seams, summary, record=not with_key)
97
+ return
98
+ if with_key and not publishes:
99
+ return
100
+ raise CliError(ExitCode.CONFIRMATION_REQUIRED, "confirmation_required", _missing(summary, with_key=with_key))
101
+
102
+
103
+ def _missing(summary: str, *, with_key: bool) -> str:
104
+ if with_key:
105
+ return f"{summary} Nothing was sent: re-run with --yes, using an API key that has the publish scope."
106
+ return (f"{summary} Nothing was sent: this needs your confirmation. Run it in a terminal and answer, or run "
107
+ "`recala grant` once so agents can write with this sign-in. (--yes works only with an API key that "
108
+ "has the publish scope.)")
109
+
110
+
111
+ def _require_publish_scope(seams: dict[str, Any], client: RecalaClient | None) -> None:
112
+ scopes = seams.get("key_scopes")
113
+ if scopes is None and client is not None:
114
+ me = client.call(ME) or {}
115
+ scopes = set((me.get("credential") or {}).get("scopes") or [])
116
+ seams["key_scopes"] = scopes
117
+ if PUBLISH_SCOPE not in (scopes or set()):
118
+ raise CliError(ExitCode.CONFIRMATION_REQUIRED, "yes_needs_publish_scope",
119
+ "--yes is accepted only from an API key with the publish scope, and this key does not hold "
120
+ "it. Nothing was sent.")
121
+
122
+
123
+ def _ask(seams: dict[str, Any], summary: str, *, record: bool) -> None:
124
+ answer = "no"
125
+ for _ in range(_ATTEMPTS):
126
+ raw = click.prompt(f" {summary} [y]es / [n]o / [a]ll", default="", show_default=False, err=True)
127
+ if raw.strip().lower() in _ANSWERS:
128
+ answer = _ANSWERS[raw.strip().lower()]
129
+ break
130
+ if answer == "no":
131
+ raise CliError(ExitCode.CONFIRMATION_REQUIRED, "declined", "Not done: you answered no. Nothing was sent.")
132
+ if answer == "all":
133
+ seams["all_confirmed"] = True
134
+ if record and auth.session_id():
135
+ record_grant()
136
+ notify(f" Confirmed: every write for this sign-in (until logout or {GRANT_HOURS} hours). "
137
+ "`recala grant --revoke` undoes it.")
recala_cli/display.py ADDED
@@ -0,0 +1,40 @@
1
+ """Untrusted terminal text, sanitized before Click can render it."""
2
+
3
+ import re
4
+ import unicodedata
5
+ from typing import Any
6
+
7
+ _ESCAPES = re.compile(
8
+ r"(?:\x1b\]|\x9d).*?(?:\x07|\x1b\\|\x9c|$)"
9
+ r"|(?:\x1b[P^_X]|[\x90\x98\x9e\x9f]).*?(?:\x1b\\|\x9c|$)"
10
+ r"|(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]"
11
+ r"|\x1b[ -/]*[@-~]",
12
+ re.DOTALL,
13
+ )
14
+
15
+
16
+ def terminal_text(value: str, *, keep_layout: bool = False) -> str:
17
+ """Remove terminal commands and Unicode controls, including bidi overrides."""
18
+ text = _ESCAPES.sub("", value)
19
+ return "".join(
20
+ ch if not unicodedata.category(ch).startswith("C") or (keep_layout and ch in "\n\t")
21
+ else " " if ch.isspace() else ""
22
+ for ch in text
23
+ )
24
+
25
+
26
+ def display_name(value: Any) -> str:
27
+ return " ".join(terminal_text(value).split()) if isinstance(value, str) else ""
28
+
29
+
30
+ def human_data(value: Any, *, field: str = "") -> Any:
31
+ """A sanitized presentation copy; the API data still goes unchanged to JSON."""
32
+ if isinstance(value, str):
33
+ if field in ("name", "display_name", "article_title", "title", "topic", "filename", "company", "author_name"):
34
+ return display_name(value)
35
+ return terminal_text(value, keep_layout=field in ("markdown", "markdown_content"))
36
+ if isinstance(value, dict):
37
+ return {terminal_text(str(key)): human_data(item, field=str(key)) for key, item in value.items()}
38
+ if isinstance(value, list):
39
+ return [human_data(item, field=field) for item in value]
40
+ return value
@@ -0,0 +1,258 @@
1
+ """Every API route the CLI calls, in one table.
2
+
3
+ apps/api/tests/test_cli_calls_only_routes_the_api_serves.py reads this table
4
+ against the live FastAPI app: a live endpoint must be served with its method, and
5
+ a ``planned`` one must not exist yet. Commands never spell a path inline.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from urllib.parse import quote
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Endpoint:
16
+ method: str
17
+ path: str
18
+ planned: bool = False # designed (DESIGN.md) but not in the API yet
19
+ changes_nothing: bool = False # a POST that only prices, signs in or polls: no answer means "try again"
20
+
21
+ @property
22
+ def writes(self) -> bool:
23
+ return self.method != "GET" and not self.changes_nothing
24
+
25
+ def url_path(self, **params: str) -> str:
26
+ return self.path.format(**{name: quote(value, safe="") for name, value in params.items()})
27
+
28
+
29
+ ORG_SHOW = Endpoint("GET", "/api/v1/orgs/{org_id}")
30
+ BILLING_CREDIT_SUMMARY = Endpoint("GET", "/api/v1/billing/credits/summary")
31
+ BILLING_CREDITS = Endpoint("GET", "/api/v1/billing/credits")
32
+ BILLING_SUBSCRIPTION = Endpoint("GET", "/api/v1/billing/subscription")
33
+ BILLING_MANAGED_USAGE = Endpoint("GET", "/api/v1/billing/managed-usage")
34
+ BILLING_PAY_LINK = Endpoint("POST", "/api/v1/billing/subscription/pay-link")
35
+ TOPICS_LIST = Endpoint("GET", "/api/v1/content-queue")
36
+
37
+ ME = Endpoint("GET", "/api/v1/me")
38
+ DEVICE_START = Endpoint("POST", "/api/v1/cli/device", changes_nothing=True)
39
+ DEVICE_TOKEN = Endpoint("POST", "/api/v1/cli/device/token", changes_nothing=True)
40
+ TOKEN_REFRESH = Endpoint("POST", "/api/v1/cli/token/refresh", changes_nothing=True)
41
+ SIGN_OUT = Endpoint("POST", "/api/v1/cli/logout")
42
+ KEYS_LIST = Endpoint("GET", "/api/v1/keys")
43
+ KEYS_CREATE = Endpoint("POST", "/api/v1/keys")
44
+ KEY_REVOKE = Endpoint("DELETE", "/api/v1/keys/{key_id}")
45
+ KEY_EVENTS = Endpoint("GET", "/api/v1/keys/{key_id}/events")
46
+
47
+ # Slice 3: content verbs.
48
+ ARTICLES_LIST = Endpoint("GET", "/api/v1/articles")
49
+ ARTICLE_SHOW = Endpoint("GET", "/api/v1/articles/{article_id}")
50
+ ARTICLE_VERSIONS = Endpoint("GET", "/api/v1/articles/{article_id}/versions")
51
+ ARTICLE_VERSION = Endpoint("GET", "/api/v1/articles/{article_id}/versions/{number}")
52
+ ARTICLE_BODY = Endpoint("PATCH", "/api/v1/articles/{article_id}/body")
53
+ ARTICLE_REDRAFT_START = Endpoint("POST", "/api/v1/articles/{article_id}/redraft")
54
+ ARTICLE_REDRAFT_LATEST = Endpoint("GET", "/api/v1/articles/{article_id}/redraft")
55
+ ARTICLE_REDRAFT_APPLY = Endpoint("POST", "/api/v1/articles/{article_id}/redraft/{redraft_id}/apply")
56
+ ARTICLE_PERSON = Endpoint("POST", "/api/v1/articles/{article_id}/person")
57
+ ARTICLE_SUBMIT = Endpoint("POST", "/api/v1/articles/{article_id}/submit-review")
58
+ ARTICLE_REQUEST_CHANGES = Endpoint("POST", "/api/v1/articles/{article_id}/request-changes")
59
+ ARTICLE_COMMENTS = Endpoint("GET", "/api/v1/articles/{article_id}/review-comments")
60
+ REVIEW_LINK_SHOW = Endpoint("GET", "/api/v1/articles/{article_id}/review-link")
61
+ REVIEW_LINK_CREATE = Endpoint("POST", "/api/v1/articles/{article_id}/review-link")
62
+ REVIEW_LINK_REVOKE = Endpoint("DELETE", "/api/v1/articles/{article_id}/review-link")
63
+ SHARE_TOKEN_ROTATE = Endpoint("POST", "/api/v1/articles/{article_id}/rotate-share-token")
64
+ TOPIC_ADD = Endpoint("POST", "/api/v1/content-queue")
65
+ TOPIC_DISMISS = Endpoint("POST", "/api/v1/content-queue/{queue_id}/dismiss")
66
+ TOPIC_APPROVE = Endpoint("POST", "/api/v1/content-queue/{queue_id}/approve")
67
+ TOPIC_WHY = Endpoint("GET", "/api/v1/content-queue/{item_id}/rationale")
68
+ KNOWLEDGE_LIST = Endpoint("GET", "/api/v1/knowledge/documents")
69
+ KNOWLEDGE_UPLOAD = Endpoint("POST", "/api/v1/knowledge/upload")
70
+ KNOWLEDGE_URL = Endpoint("POST", "/api/v1/knowledge/extract-url")
71
+ KNOWLEDGE_URLS = Endpoint("POST", "/api/v1/knowledge/extract-urls")
72
+ KNOWLEDGE_REMOVE = Endpoint("DELETE", "/api/v1/knowledge/documents/{doc_id}")
73
+ KNOWLEDGE_PAGE = Endpoint("GET", "/api/v1/knowledge/documents/{doc_id}/page")
74
+ PEOPLE_LIST = Endpoint("GET", "/api/v1/people")
75
+ CALENDAR = Endpoint("GET", "/api/v1/publication-calendar")
76
+ AUTHORITY_SCORE = Endpoint("GET", "/api/v1/authority-score")
77
+
78
+ # Slice 4: approving, publishing, spending, connecting.
79
+ ARTICLE_APPROVE = Endpoint("POST", "/api/v1/articles/{article_id}/approve")
80
+ ARTICLE_UNPUBLISH = Endpoint("POST", "/api/v1/articles/{article_id}/unpublish")
81
+ ARTICLE_REPUBLISH = Endpoint("POST", "/api/v1/articles/{article_id}/republish")
82
+ ARTICLE_READINESS = Endpoint("GET", "/api/v1/articles/{article_id}/publication-readiness")
83
+ APPOINTMENT_BOOK = Endpoint("PUT", "/api/v1/articles/{article_id}/publication-appointment")
84
+ APPOINTMENT_CANCEL = Endpoint("DELETE", "/api/v1/articles/{article_id}/publication-appointment")
85
+ COST_PREVIEW = Endpoint("POST", "/api/v1/cost-previews", changes_nothing=True)
86
+ GENERATE_FROM_TOPIC = Endpoint("POST", "/api/v1/generate/from-queue/{queue_id}")
87
+ BILLING_SPEND = Endpoint("GET", "/api/v1/billing/spend")
88
+ CMS_STATUS = Endpoint("GET", "/api/v1/cms/status")
89
+ GSC_STATUS = Endpoint("GET", "/api/v1/gsc/status")
90
+ GSC_SERVICE_ACCOUNT = Endpoint("GET", "/api/v1/gsc/service-account")
91
+ GSC_SERVICE_ACCOUNT_CHECK = Endpoint("POST", "/api/v1/gsc/service-account/check")
92
+ GSC_PERFORMANCE = Endpoint("GET", "/api/v1/gsc/performance")
93
+ BING_STATUS = Endpoint("GET", "/api/v1/bing/status")
94
+ BING_SITES = Endpoint("GET", "/api/v1/bing/sites")
95
+ BING_SITE = Endpoint("PUT", "/api/v1/bing/site")
96
+ BING_PERFORMANCE = Endpoint("GET", "/api/v1/bing/performance")
97
+
98
+ # Onboarding from the terminal: the routes the portal's wizard, Settings and billing pages call.
99
+ ORG_CREATE = Endpoint("POST", "/api/v1/orgs")
100
+ ORG_UPDATE = Endpoint("PATCH", "/api/v1/orgs/{org_id}")
101
+ ONBOARDING_SCRAPE = Endpoint("POST", "/api/v1/onboarding/scrape")
102
+ ONBOARDING_SAVE = Endpoint("POST", "/api/v1/onboarding/save-entities")
103
+ ONBOARDING_COMPLETE = Endpoint("POST", "/api/v1/onboarding/complete")
104
+ PERSON_CREATE = Endpoint("POST", "/api/v1/people")
105
+ PERSON_UPDATE = Endpoint("PATCH", "/api/v1/people/{person_id}")
106
+ KNOWLEDGE_ADMIT = Endpoint("POST", "/api/v1/knowledge/documents/{doc_id}/admit")
107
+ SANITY_STATUS = Endpoint("GET", "/api/v1/cms/sanity/status")
108
+ SANITY_CONNECT = Endpoint("POST", "/api/v1/cms/sanity/connect")
109
+ WORDPRESS_VERIFY = Endpoint("POST", "/api/v1/wordpress/verify", changes_nothing=True)
110
+ WORDPRESS_CONNECT = Endpoint("POST", "/api/v1/wordpress/connect")
111
+ GSC_PROPERTIES = Endpoint("GET", "/api/v1/gsc/properties")
112
+ GSC_PROPERTY = Endpoint("PUT", "/api/v1/gsc/property")
113
+ CREDITS_SUMMARY = Endpoint("GET", "/api/v1/billing/credits/summary")
114
+ ENTITLEMENTS = Endpoint("GET", "/api/v1/billing/entitlements")
115
+ QUESTION_PANEL = Endpoint("GET", "/api/v1/question-panel")
116
+ QUESTION_PANEL_BASELINE = Endpoint("POST", "/api/v1/question-panel/baseline")
117
+
118
+ # Indexing routes are served by main; feature flags can still return 404 at runtime.
119
+ INDEXING_SITEMAPS = Endpoint("GET", "/api/v1/indexing/sitemaps")
120
+ INDEXING_SITEMAPS_SUBMIT = Endpoint("POST", "/api/v1/indexing/sitemaps/submit")
121
+ ARTICLE_INDEXING = Endpoint("GET", "/api/v1/articles/{article_id}/indexing")
122
+ ARTICLE_INDEXING_SUBMIT = Endpoint("POST", "/api/v1/articles/{article_id}/indexing/submit")
123
+
124
+ TOPICS_SCOUT = Endpoint("POST", "/api/v1/topics/scout")
125
+ TOPICS_SCOUT_STATUS = Endpoint("GET", "/api/v1/topics/scout/status")
126
+
127
+ CADENCE_SHOW = Endpoint("GET", "/api/v1/publication-cadence")
128
+ CADENCE_SET = Endpoint("PUT", "/api/v1/publication-cadence")
129
+ AUTOAPPROVE_SHOW = Endpoint("GET", "/api/v1/orgs/current/auto-approve")
130
+ AUTOAPPROVE_SET = Endpoint("PUT", "/api/v1/orgs/current/auto-approve")
131
+ FAITHFUL_TRANSLATIONS_SHOW = Endpoint(
132
+ "GET", "/api/v1/orgs/current/auto-approve/faithful-translations"
133
+ )
134
+ FAITHFUL_TRANSLATIONS_SET = Endpoint(
135
+ "PUT", "/api/v1/orgs/current/auto-approve/faithful-translations"
136
+ )
137
+ PUBLICATION_PLAN = Endpoint("GET", "/api/v1/publication-plan")
138
+ ESCALATED_SLOTS = Endpoint("GET", "/api/v1/publication-slots/escalated")
139
+ SLOT_TOPIC_REPLACE = Endpoint("POST", "/api/v1/publication-slots/{slot_id}/topic")
140
+
141
+ GENERATION_STATUS = Endpoint("GET", "/api/v1/runs/{queue_id}")
142
+ ARTICLE_REGENERATE = Endpoint("POST", "/api/v1/articles/{article_id}/regenerate")
143
+ ARTICLE_RECHECK = Endpoint("POST", "/api/v1/articles/{article_id}/recheck")
144
+ UP_NEXT_ADD = Endpoint("POST", "/api/v1/up-next/add")
145
+ UP_NEXT_REMOVE = Endpoint("POST", "/api/v1/up-next/remove")
146
+ UP_NEXT_REORDER = Endpoint("POST", "/api/v1/up-next/reorder")
147
+
148
+ # 0.6.0: what the portal does that the terminal could not.
149
+ LANGUAGES_SHOW = Endpoint("GET", "/api/v1/org-settings/languages")
150
+ LANGUAGES_SET = Endpoint("PATCH", "/api/v1/org-settings/languages")
151
+ NOTIFICATIONS_LIST = Endpoint("GET", "/api/v1/notifications")
152
+ NOTIFICATION_READ = Endpoint("POST", "/api/v1/notifications/{notification_id}/read")
153
+ NOTIFICATIONS_READ_ALL = Endpoint("POST", "/api/v1/notifications/read-all")
154
+ REPORT_MONTHLY = Endpoint("GET", "/api/v1/reports/monthly")
155
+ REPORT_SHARES = Endpoint("GET", "/api/v1/reports/monthly/shares")
156
+ REPORT_SHARE_CREATE = Endpoint("POST", "/api/v1/reports/monthly/share")
157
+ REFRESH_SUMMARY = Endpoint("GET", "/api/v1/billing/refresh-summary")
158
+ ARTICLE_AUTO_REFRESH = Endpoint("PATCH", "/api/v1/articles/{article_id}/auto-refresh")
159
+ REFRESH_STATUS = Endpoint("GET", "/api/v1/refreshes/{refresh_id}/status")
160
+ PROFILE_AUTHOR = Endpoint("GET", "/api/v1/author")
161
+ PROFILE_POSITIONING = Endpoint("GET", "/api/v1/org-profile/positioning")
162
+ PROFILE_INTENT = Endpoint("GET", "/api/v1/intent")
163
+ BILLING_INVOICES = Endpoint("GET", "/api/v1/billing/invoices")
164
+
165
+ ALL_ENDPOINTS = (
166
+ LANGUAGES_SHOW, LANGUAGES_SET, NOTIFICATIONS_LIST, NOTIFICATION_READ, NOTIFICATIONS_READ_ALL,
167
+ REPORT_MONTHLY, REPORT_SHARES, REPORT_SHARE_CREATE, REFRESH_SUMMARY, ARTICLE_AUTO_REFRESH, REFRESH_STATUS,
168
+ PROFILE_AUTHOR, PROFILE_POSITIONING, PROFILE_INTENT, BILLING_INVOICES,
169
+ GENERATION_STATUS, ARTICLE_REGENERATE, ARTICLE_RECHECK,
170
+ UP_NEXT_ADD, UP_NEXT_REMOVE, UP_NEXT_REORDER,
171
+ TOPICS_SCOUT,
172
+ TOPICS_SCOUT_STATUS,
173
+ CADENCE_SHOW, CADENCE_SET, AUTOAPPROVE_SHOW, AUTOAPPROVE_SET, PUBLICATION_PLAN,
174
+ ESCALATED_SLOTS, SLOT_TOPIC_REPLACE,
175
+ FAITHFUL_TRANSLATIONS_SHOW, FAITHFUL_TRANSLATIONS_SET,
176
+ ORG_SHOW,
177
+ BILLING_CREDITS,
178
+ BILLING_CREDIT_SUMMARY,
179
+ BILLING_SUBSCRIPTION,
180
+ BILLING_MANAGED_USAGE,
181
+ TOPICS_LIST,
182
+ ME,
183
+ DEVICE_START,
184
+ DEVICE_TOKEN,
185
+ TOKEN_REFRESH,
186
+ SIGN_OUT,
187
+ KEYS_LIST,
188
+ KEYS_CREATE,
189
+ KEY_REVOKE,
190
+ KEY_EVENTS,
191
+ ARTICLES_LIST,
192
+ ARTICLE_SHOW,
193
+ ARTICLE_VERSIONS,
194
+ ARTICLE_VERSION,
195
+ ARTICLE_BODY,
196
+ ARTICLE_REDRAFT_START,
197
+ ARTICLE_REDRAFT_LATEST,
198
+ ARTICLE_REDRAFT_APPLY,
199
+ ARTICLE_PERSON,
200
+ ARTICLE_SUBMIT,
201
+ ARTICLE_REQUEST_CHANGES,
202
+ ARTICLE_COMMENTS,
203
+ REVIEW_LINK_SHOW,
204
+ REVIEW_LINK_CREATE,
205
+ REVIEW_LINK_REVOKE,
206
+ SHARE_TOKEN_ROTATE,
207
+ TOPIC_ADD,
208
+ TOPIC_DISMISS,
209
+ TOPIC_APPROVE,
210
+ TOPIC_WHY,
211
+ KNOWLEDGE_LIST,
212
+ KNOWLEDGE_UPLOAD,
213
+ KNOWLEDGE_URL,
214
+ KNOWLEDGE_URLS,
215
+ KNOWLEDGE_REMOVE,
216
+ KNOWLEDGE_PAGE,
217
+ PEOPLE_LIST,
218
+ CALENDAR,
219
+ AUTHORITY_SCORE,
220
+ ARTICLE_APPROVE,
221
+ ARTICLE_UNPUBLISH,
222
+ ARTICLE_REPUBLISH,
223
+ ARTICLE_READINESS,
224
+ APPOINTMENT_BOOK,
225
+ APPOINTMENT_CANCEL,
226
+ COST_PREVIEW,
227
+ GENERATE_FROM_TOPIC,
228
+ BILLING_SPEND,
229
+ CMS_STATUS,
230
+ GSC_STATUS,
231
+ GSC_SERVICE_ACCOUNT,
232
+ GSC_SERVICE_ACCOUNT_CHECK,
233
+ GSC_PERFORMANCE,
234
+ BING_STATUS,
235
+ BING_PERFORMANCE,
236
+ ORG_CREATE,
237
+ ORG_UPDATE,
238
+ ONBOARDING_SCRAPE,
239
+ ONBOARDING_SAVE,
240
+ ONBOARDING_COMPLETE,
241
+ PERSON_CREATE,
242
+ PERSON_UPDATE,
243
+ KNOWLEDGE_ADMIT,
244
+ SANITY_STATUS,
245
+ SANITY_CONNECT,
246
+ WORDPRESS_VERIFY,
247
+ WORDPRESS_CONNECT,
248
+ GSC_PROPERTIES,
249
+ GSC_PROPERTY,
250
+ CREDITS_SUMMARY,
251
+ ENTITLEMENTS,
252
+ QUESTION_PANEL,
253
+ QUESTION_PANEL_BASELINE,
254
+ INDEXING_SITEMAPS,
255
+ INDEXING_SITEMAPS_SUBMIT,
256
+ ARTICLE_INDEXING,
257
+ ARTICLE_INDEXING_SUBMIT,
258
+ )