logion-cli 0.1.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 (135) hide show
  1. cli/__init__.py +2 -0
  2. cli/_config.py +51 -0
  3. cli/_confirm.py +16 -0
  4. cli/_context.py +17 -0
  5. cli/_course_bundle.py +46 -0
  6. cli/_course_capabilities.py +580 -0
  7. cli/_credentials.py +104 -0
  8. cli/_errors.py +82 -0
  9. cli/_first_run.py +90 -0
  10. cli/_harness/__init__.py +68 -0
  11. cli/_harness/base.py +106 -0
  12. cli/_harness/claude_code.py +168 -0
  13. cli/_harness/codex.py +79 -0
  14. cli/_harness/custom.py +55 -0
  15. cli/_harness/hermes.py +93 -0
  16. cli/_harness/opencode.py +255 -0
  17. cli/_local_state.py +1053 -0
  18. cli/_options.py +36 -0
  19. cli/_output.py +47 -0
  20. cli/_parser.py +73 -0
  21. cli/_recall_calibration.py +90 -0
  22. cli/_recall_ranker.py +74 -0
  23. cli/_taxonomy.py +120 -0
  24. cli/_update_policy.py +152 -0
  25. cli/_utils.py +16 -0
  26. cli/_version.py +26 -0
  27. cli/commands/__init__.py +2 -0
  28. cli/commands/admin.py +535 -0
  29. cli/commands/bounties.py +490 -0
  30. cli/commands/course_reviews/__init__.py +6 -0
  31. cli/commands/course_reviews/_download_handler.py +104 -0
  32. cli/commands/course_reviews/_render.py +129 -0
  33. cli/commands/course_reviews/handlers.py +197 -0
  34. cli/commands/course_reviews/parser.py +93 -0
  35. cli/commands/courses/__init__.py +6 -0
  36. cli/commands/courses/_capability_render.py +183 -0
  37. cli/commands/courses/_cmd_help.py +18 -0
  38. cli/commands/courses/_purchase.py +76 -0
  39. cli/commands/courses/_review_helpers.py +93 -0
  40. cli/commands/courses/_taxonomy_data.py +173 -0
  41. cli/commands/courses/_upload_bundle_validation.py +28 -0
  42. cli/commands/courses/_uploads_push.py +243 -0
  43. cli/commands/courses/capabilities.py +250 -0
  44. cli/commands/courses/capability_frontmatter.py +150 -0
  45. cli/commands/courses/handlers.py +50 -0
  46. cli/commands/courses/mutations.py +217 -0
  47. cli/commands/courses/parser.py +66 -0
  48. cli/commands/courses/parser_capabilities.py +95 -0
  49. cli/commands/courses/parser_sections.py +239 -0
  50. cli/commands/courses/parser_uploads.py +84 -0
  51. cli/commands/courses/parser_utils.py +65 -0
  52. cli/commands/courses/publication.py +60 -0
  53. cli/commands/courses/report_usage.py +131 -0
  54. cli/commands/courses/reviews.py +237 -0
  55. cli/commands/courses/taxonomy_handler.py +61 -0
  56. cli/commands/courses/taxonomy_suggest.py +197 -0
  57. cli/commands/courses/uploads.py +142 -0
  58. cli/commands/courses/versions.py +65 -0
  59. cli/commands/credits/__init__.py +6 -0
  60. cli/commands/credits/_helpers.py +153 -0
  61. cli/commands/credits/handlers.py +218 -0
  62. cli/commands/credits/parser.py +115 -0
  63. cli/commands/docs/__init__.py +6 -0
  64. cli/commands/docs/handlers.py +137 -0
  65. cli/commands/docs/parser.py +27 -0
  66. cli/commands/health/__init__.py +6 -0
  67. cli/commands/health/handlers.py +26 -0
  68. cli/commands/health/parser.py +20 -0
  69. cli/commands/identity/__init__.py +6 -0
  70. cli/commands/identity/_autopost.py +97 -0
  71. cli/commands/identity/_closing_copy.py +89 -0
  72. cli/commands/identity/_companion.py +232 -0
  73. cli/commands/identity/_companion_source.py +135 -0
  74. cli/commands/identity/_harness_select.py +85 -0
  75. cli/commands/identity/_onboarding_helpers.py +168 -0
  76. cli/commands/identity/handlers.py +173 -0
  77. cli/commands/identity/onboarding.py +246 -0
  78. cli/commands/identity/parser.py +72 -0
  79. cli/commands/listings/__init__.py +6 -0
  80. cli/commands/listings/handlers.py +135 -0
  81. cli/commands/listings/parser.py +57 -0
  82. cli/commands/notifications/__init__.py +6 -0
  83. cli/commands/notifications/handlers.py +120 -0
  84. cli/commands/notifications/parser.py +49 -0
  85. cli/commands/payments/__init__.py +6 -0
  86. cli/commands/payments/_orders_helpers.py +114 -0
  87. cli/commands/payments/handlers.py +138 -0
  88. cli/commands/payments/parser.py +97 -0
  89. cli/commands/recall/__init__.py +7 -0
  90. cli/commands/recall/handlers.py +87 -0
  91. cli/commands/recall/parser.py +70 -0
  92. cli/commands/referrals/__init__.py +6 -0
  93. cli/commands/referrals/_helpers.py +63 -0
  94. cli/commands/referrals/handlers.py +100 -0
  95. cli/commands/referrals/parser.py +65 -0
  96. cli/commands/reports/__init__.py +6 -0
  97. cli/commands/reports/handlers.py +57 -0
  98. cli/commands/reports/parser.py +52 -0
  99. cli/commands/skills/__init__.py +7 -0
  100. cli/commands/skills/_agent_symlink.py +161 -0
  101. cli/commands/skills/_finalize.py +112 -0
  102. cli/commands/skills/_inspect_handler.py +218 -0
  103. cli/commands/skills/_install_helpers.py +186 -0
  104. cli/commands/skills/_query_handlers.py +83 -0
  105. cli/commands/skills/_search_handler.py +136 -0
  106. cli/commands/skills/_update_handler.py +110 -0
  107. cli/commands/skills/_verify_handler.py +109 -0
  108. cli/commands/skills/handlers.py +202 -0
  109. cli/commands/skills/parser.py +154 -0
  110. cli/commands/workspace.py +406 -0
  111. cli/docs/README.md +5 -0
  112. cli/docs/__init__.py +1 -0
  113. cli/docs/bounties-and-referrals.md +18 -0
  114. cli/docs/concepts.md +47 -0
  115. cli/docs/creating-courses.md +25 -0
  116. cli/docs/credits-and-purchases.md +30 -0
  117. cli/docs/credits-terms.md +23 -0
  118. cli/docs/getting-started.md +95 -0
  119. cli/docs/marketplace-loop.md +108 -0
  120. cli/docs/privacy.md +30 -0
  121. cli/docs/referral-terms.md +24 -0
  122. cli/docs/reviews.md +47 -0
  123. cli/docs/safety.md +28 -0
  124. cli/docs/terms.md +54 -0
  125. cli/main.py +84 -0
  126. cli/templates/__init__.py +2 -0
  127. cli/templates/course_capabilities.template.yaml +189 -0
  128. cli/templates/course_license_apache-2.0.template.txt +30 -0
  129. cli/templates/course_license_logion-standard-course-v1.template.txt +49 -0
  130. cli/templates/course_license_mit.template.txt +21 -0
  131. logion_cli-0.1.0.dist-info/METADATA +49 -0
  132. logion_cli-0.1.0.dist-info/RECORD +135 -0
  133. logion_cli-0.1.0.dist-info/WHEEL +4 -0
  134. logion_cli-0.1.0.dist-info/entry_points.txt +4 -0
  135. logion_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,6 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Payments command package."""
3
+
4
+ from .parser import register
5
+
6
+ __all__ = ["register"]
@@ -0,0 +1,114 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Shared helpers for payments order commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import sys
8
+ from typing import Any
9
+ from uuid import UUID
10
+
11
+ from cli._config import CliConfig
12
+ from cli._errors import emit_error_json
13
+ from cli._output import emit_json
14
+
15
+ TERMINAL_STATUSES = frozenset({"fulfilled", "failed", "refunded", "cancelled"})
16
+
17
+
18
+ def invalid_identifier(
19
+ args: argparse.Namespace,
20
+ label: str,
21
+ value: str,
22
+ exit_code: int = 2,
23
+ ) -> int:
24
+ """Emit an unsafe identifier error envelope when JSON was requested."""
25
+ message = f"{label} must be a valid UUID (got: {value!r})."
26
+ if getattr(args, "json_output", False):
27
+ emit_error_json("unsafe_identifier", message, exit_code)
28
+ else:
29
+ sys.stderr.write(f"Error: {message}\n")
30
+ return exit_code
31
+
32
+
33
+ def validate_uuid_arg(
34
+ args: argparse.Namespace, value: str, label: str
35
+ ) -> int | None:
36
+ """Validate a UUID-ish positional argument."""
37
+ if not value or not value.strip():
38
+ return invalid_identifier(args, label, value)
39
+ try:
40
+ UUID(value)
41
+ except ValueError:
42
+ return invalid_identifier(args, label, value)
43
+ return None
44
+
45
+
46
+ def order_to_payload(result: Any) -> dict[str, Any]:
47
+ """Normalize SDK order responses to the public CLI shape."""
48
+ raw = (
49
+ result.model_dump(mode="json")
50
+ if hasattr(result, "model_dump")
51
+ else dict(result)
52
+ )
53
+ settled_at = (
54
+ raw.get("settled_at")
55
+ or raw.get("paid_at")
56
+ or raw.get("transferred_at")
57
+ )
58
+ payload: dict[str, Any] = {
59
+ "order_id": raw.get("order_id") or raw.get("id"),
60
+ "status": raw.get("status"),
61
+ "course_id": raw.get("course_id"),
62
+ "amount_cents": raw.get("amount_cents"),
63
+ "currency": raw.get("currency"),
64
+ "settled_at": settled_at,
65
+ "purchase_flow": raw.get("purchase_flow"),
66
+ "balance_before_cents": raw.get("balance_before_cents"),
67
+ "balance_after_cents": raw.get("balance_after_cents"),
68
+ }
69
+ # Fold through any extra fields the SDK exposes
70
+ for key, value in raw.items():
71
+ payload.setdefault(key, value)
72
+ return payload
73
+
74
+
75
+ def emit_wait_result(
76
+ config: CliConfig,
77
+ payload: dict[str, Any],
78
+ elapsed: float,
79
+ *,
80
+ final: bool,
81
+ ) -> None:
82
+ """Emit the current wait-state payload."""
83
+ wait_payload = {
84
+ **payload,
85
+ "elapsed_seconds": round(elapsed, 2),
86
+ "terminal": payload.get("status") in TERMINAL_STATUSES,
87
+ "final": final,
88
+ }
89
+ if config.json_output:
90
+ emit_json("logion.payments.orders.wait", wait_payload)
91
+ sys.stdout.write("\n")
92
+ return
93
+
94
+ summary = (
95
+ f"Order {payload.get('order_id')}: "
96
+ f"status={payload.get('status')} (elapsed {int(elapsed)}s)"
97
+ )
98
+ sys.stdout.write(summary)
99
+ sys.stdout.write("\n")
100
+
101
+
102
+ def timeout_payload(order_id: str) -> dict[str, Any]:
103
+ """Return the fallback payload for an unfinished wait call."""
104
+ return {
105
+ "order_id": order_id,
106
+ "status": "unknown",
107
+ "course_id": None,
108
+ "amount_cents": None,
109
+ "currency": None,
110
+ "settled_at": None,
111
+ "purchase_flow": None,
112
+ "balance_before_cents": None,
113
+ "balance_after_cents": None,
114
+ }
@@ -0,0 +1,138 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Handlers for payments commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+
8
+ from cli._config import resolve_config_from_args
9
+ from cli._confirm import require_yes
10
+ from cli._context import make_client
11
+ from cli._errors import handle_error, print_err
12
+ from cli._output import emit, emit_json, to_data
13
+
14
+ from ._orders_helpers import (
15
+ order_to_payload,
16
+ validate_uuid_arg,
17
+ )
18
+
19
+
20
+ def handle_seller_readiness(args: argparse.Namespace) -> int:
21
+ """Execute the payments seller-readiness command."""
22
+ config = resolve_config_from_args(args)
23
+ client = make_client(config)
24
+ try:
25
+ result = client.v1.payments.get_seller_readiness()
26
+ if config.json_output:
27
+ emit_json("logion.payments.seller-readiness", to_data(result))
28
+ else:
29
+ emit(result, json_output=False)
30
+ except Exception as exc:
31
+ return handle_error(exc)
32
+ else:
33
+ return 0
34
+ finally:
35
+ client.close()
36
+
37
+
38
+ def handle_onboarding_link(args: argparse.Namespace) -> int:
39
+ """Execute the payments onboarding-link command."""
40
+ config = resolve_config_from_args(args)
41
+ client = make_client(config)
42
+ try:
43
+ result = client.v1.payments.create_onboarding_link()
44
+ if config.json_output:
45
+ emit_json("logion.payments.onboarding-link", to_data(result))
46
+ else:
47
+ emit(result, json_output=False)
48
+ except Exception as exc:
49
+ return handle_error(exc)
50
+ else:
51
+ return 0
52
+ finally:
53
+ client.close()
54
+
55
+
56
+ def handle_orders_get(args: argparse.Namespace) -> int:
57
+ """Execute the payments orders get command."""
58
+ bad_id = validate_uuid_arg(args, args.order_id, "ORDER_ID")
59
+ if bad_id is not None:
60
+ return bad_id
61
+ config = resolve_config_from_args(args)
62
+ client = make_client(config)
63
+ try:
64
+ result = client.v1.payments.get_order(order_id=args.order_id)
65
+ except Exception as exc:
66
+ return handle_error(exc)
67
+ else:
68
+ payload = order_to_payload(result)
69
+ if config.json_output:
70
+ emit_json("logion.payments.orders.get", payload)
71
+ else:
72
+ emit(payload, json_output=False)
73
+ return 0
74
+ finally:
75
+ client.close()
76
+
77
+
78
+ def handle_creator_earnings(args: argparse.Namespace) -> int:
79
+ """Execute the payments creator-earnings command."""
80
+ config = resolve_config_from_args(args)
81
+ client = make_client(config)
82
+ try:
83
+ result = client.v1.payments.get_creator_earnings()
84
+ if config.json_output:
85
+ emit_json("logion.payments.creator-earnings", to_data(result))
86
+ else:
87
+ emit(result, json_output=False)
88
+ except Exception as exc:
89
+ return handle_error(exc)
90
+ else:
91
+ return 0
92
+ finally:
93
+ client.close()
94
+
95
+
96
+ def handle_cash_out(args: argparse.Namespace) -> int:
97
+ """Execute the payments cash-out command."""
98
+ if not args.dry_run:
99
+ refusal = require_yes(args.yes, "initiate a cash-out transfer")
100
+ if refusal is not None:
101
+ return refusal
102
+ if args.expected_gross_payout_cents is None:
103
+ print_err(
104
+ "Error: non-dry-run cash-out requires "
105
+ "--expected-gross-payout-cents from a prior preview."
106
+ )
107
+ return 2
108
+ config = resolve_config_from_args(args)
109
+ client = make_client(config)
110
+ try:
111
+ if not args.dry_run:
112
+ preview = client.v1.payments.request_cash_out(
113
+ minimum_payout_cents=args.minimum_payout_cents,
114
+ dry_run=True,
115
+ )
116
+ preview_data = to_data(preview)
117
+ actual = preview_data.get("gross_payout_cents")
118
+ if actual != args.expected_gross_payout_cents:
119
+ print_err(
120
+ "Error: cash-out preview changed: expected "
121
+ f"{args.expected_gross_payout_cents} cents, got "
122
+ f"{actual} cents. Review the new dry-run before retrying."
123
+ )
124
+ return 2
125
+ result = client.v1.payments.request_cash_out(
126
+ minimum_payout_cents=args.minimum_payout_cents,
127
+ dry_run=args.dry_run,
128
+ )
129
+ if config.json_output:
130
+ emit_json("logion.payments.cash-out", to_data(result))
131
+ else:
132
+ emit(result, json_output=False)
133
+ except Exception as exc:
134
+ return handle_error(exc)
135
+ else:
136
+ return 0
137
+ finally:
138
+ client.close()
@@ -0,0 +1,97 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Parser registration for payments commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+
8
+ from cli._options import COMMON_PARSER
9
+
10
+ from .handlers import (
11
+ handle_cash_out,
12
+ handle_creator_earnings,
13
+ handle_onboarding_link,
14
+ handle_orders_get,
15
+ handle_seller_readiness,
16
+ )
17
+
18
+
19
+ def register(subparsers: argparse._SubParsersAction) -> None:
20
+ """Register the ``payments`` subcommand group."""
21
+ parser = subparsers.add_parser(
22
+ "payments",
23
+ help="Manage payments and seller onboarding",
24
+ )
25
+ sub = parser.add_subparsers(
26
+ dest="payments_command",
27
+ required=True,
28
+ )
29
+
30
+ seller_readiness = sub.add_parser(
31
+ "seller-readiness",
32
+ help="Check seller readiness status",
33
+ parents=[COMMON_PARSER],
34
+ )
35
+ seller_readiness.set_defaults(handler=handle_seller_readiness)
36
+
37
+ onboarding_link = sub.add_parser(
38
+ "onboarding-link",
39
+ help="Create a Stripe onboarding link",
40
+ parents=[COMMON_PARSER],
41
+ )
42
+ onboarding_link.set_defaults(handler=handle_onboarding_link)
43
+
44
+ # payments creator-earnings
45
+ creator_earnings = sub.add_parser(
46
+ "creator-earnings",
47
+ help="Get creator earnings summary",
48
+ parents=[COMMON_PARSER],
49
+ )
50
+ creator_earnings.set_defaults(handler=handle_creator_earnings)
51
+
52
+ # payments cash-out
53
+ cash_out = sub.add_parser(
54
+ "cash-out",
55
+ help="Request a cash-out of accrued creator earnings",
56
+ parents=[COMMON_PARSER],
57
+ )
58
+ cash_out.add_argument(
59
+ "--minimum-payout-cents",
60
+ dest="minimum_payout_cents",
61
+ type=int,
62
+ default=None,
63
+ help="Override minimum payout threshold (in cents).",
64
+ )
65
+ cash_out.add_argument(
66
+ "--dry-run",
67
+ action="store_true",
68
+ default=False,
69
+ help="Compute result without processing the transfer.",
70
+ )
71
+ cash_out.add_argument(
72
+ "--yes",
73
+ dest="yes",
74
+ action="store_true",
75
+ help="Confirm initiating a non-dry-run cash-out transfer.",
76
+ )
77
+ cash_out.add_argument(
78
+ "--expected-gross-payout-cents",
79
+ dest="expected_gross_payout_cents",
80
+ type=int,
81
+ default=None,
82
+ help="Required for execution; must match the server dry-run preview.",
83
+ )
84
+ cash_out.set_defaults(handler=handle_cash_out)
85
+
86
+ orders = sub.add_parser("orders", help="Manage orders")
87
+ orders_sub = orders.add_subparsers(
88
+ dest="payments_orders_command",
89
+ required=True,
90
+ )
91
+ orders_get = orders_sub.add_parser(
92
+ "get",
93
+ help="Get order details",
94
+ parents=[COMMON_PARSER],
95
+ )
96
+ orders_get.add_argument("order_id", metavar="ORDER_ID")
97
+ orders_get.set_defaults(handler=handle_orders_get)
@@ -0,0 +1,7 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Recall command package — search and record locally cached evidence
3
+ of installed capabilities and proven workflows."""
4
+
5
+ from .parser import register
6
+
7
+ __all__ = ["register"]
@@ -0,0 +1,87 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Handlers for the ``recall`` command group.
3
+
4
+ Read-only fuzzy lookup over installed capabilities, prior successful
5
+ workflows, and (future) local references. Output carries
6
+ ``confidence`` (0..1), ``band`` (HIGH|MEDIUM|LOW|NONE), and
7
+ ``query_similarity``. Confidence is recomputed per query; the on-disk
8
+ index stores only the persisted prior. Recall never executes anything;
9
+ ``danger_flags`` are surfaced for the agent's confirmation gating.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import sys
16
+
17
+ from cli._local_state import (
18
+ ensure_layout,
19
+ record_workflow_success,
20
+ search_recall,
21
+ )
22
+ from cli._output import emit_json
23
+
24
+
25
+ def handle_recall_search(args: argparse.Namespace) -> int:
26
+ """Search the local recall index for *query*."""
27
+ home = ensure_layout(getattr(args, "target", None))
28
+ query = args.query.strip()
29
+ payload = {
30
+ "query": args.query,
31
+ "matches": [],
32
+ "total": 0,
33
+ "limit": args.limit,
34
+ }
35
+ if not query:
36
+ if getattr(args, "json_output", False):
37
+ emit_json("logion.recall.search", payload)
38
+ return 0
39
+ print("Please clarify the recall query before searching.")
40
+ return 0
41
+ results = search_recall(query, home, limit=args.limit)
42
+ payload = {
43
+ "query": args.query,
44
+ "matches": results,
45
+ "total": len(results),
46
+ "limit": args.limit,
47
+ }
48
+ if getattr(args, "json_output", False):
49
+ emit_json("logion.recall.search", payload)
50
+ return 0
51
+ if not results:
52
+ print(f"No recall matches for {args.query!r}.")
53
+ return 0
54
+ print(f"Top {len(results)} recall matches:")
55
+ for entry in results:
56
+ line = (
57
+ f" [{entry['type']}] {entry.get('id', '?')} "
58
+ f"(confidence={entry.get('confidence', 0.0):.2f}, "
59
+ f"band={entry.get('band', 'NONE')})"
60
+ )
61
+ if entry.get("danger_flags"):
62
+ line += f" flags={','.join(entry['danger_flags'])}"
63
+ print(line)
64
+ title = entry.get("title")
65
+ if title:
66
+ print(f" {title}")
67
+ return 0
68
+
69
+
70
+ def handle_recall_record(args: argparse.Namespace) -> int:
71
+ """Record a successful workflow run into recall history."""
72
+ home = ensure_layout(getattr(args, "target", None))
73
+ if not args.command:
74
+ print(
75
+ "ERROR: at least one --command is required.",
76
+ file=sys.stderr,
77
+ )
78
+ return 2
79
+ record = record_workflow_success(
80
+ workflow_id=args.id,
81
+ title=args.title,
82
+ commands=args.command,
83
+ home=home,
84
+ confidence=args.confidence,
85
+ )
86
+ emit_json("logion.recall.record", record)
87
+ return 0
@@ -0,0 +1,70 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Parser registration for recall commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+
9
+ from cli._options import COMMON_PARSER
10
+
11
+ from .handlers import handle_recall_record, handle_recall_search
12
+
13
+
14
+ def _confidence(value: str) -> float:
15
+ """argparse ``type=`` validator: float in the closed interval [0, 1]."""
16
+ try:
17
+ f = float(value)
18
+ except ValueError as exc:
19
+ raise argparse.ArgumentTypeError(
20
+ f"--confidence must be a float, got {value!r}"
21
+ ) from exc
22
+ if not (0.0 <= f <= 1.0):
23
+ raise argparse.ArgumentTypeError(
24
+ f"--confidence must be in [0.0, 1.0], got {f}"
25
+ )
26
+ return f
27
+
28
+
29
+ def register(subparsers: argparse._SubParsersAction) -> None:
30
+ """Register the ``recall`` subcommand group."""
31
+ parser = subparsers.add_parser(
32
+ "recall",
33
+ help="Read-only fuzzy lookup over local capabilities and workflows",
34
+ )
35
+ sub = parser.add_subparsers(
36
+ dest="recall_command",
37
+ required=True,
38
+ )
39
+
40
+ search = sub.add_parser(
41
+ "search",
42
+ help="Search local recall index",
43
+ parents=[COMMON_PARSER],
44
+ )
45
+ search.add_argument("query", metavar="QUERY")
46
+ search.add_argument("--limit", type=int, default=5)
47
+ search.add_argument("--target", type=Path, default=None)
48
+ search.set_defaults(handler=handle_recall_search)
49
+
50
+ record = sub.add_parser(
51
+ "record",
52
+ help="Record a successful workflow run",
53
+ parents=[COMMON_PARSER],
54
+ )
55
+ record.add_argument("--id", required=True)
56
+ record.add_argument("--title", required=True)
57
+ record.add_argument(
58
+ "--command",
59
+ action="append",
60
+ default=[],
61
+ help="Command included in the workflow (repeatable)",
62
+ )
63
+ record.add_argument(
64
+ "--confidence",
65
+ type=_confidence,
66
+ default=0.5,
67
+ help="Initial confidence score in [0.0, 1.0]",
68
+ )
69
+ record.add_argument("--target", type=Path, default=None)
70
+ record.set_defaults(handler=handle_recall_record)
@@ -0,0 +1,6 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Referrals command package."""
3
+
4
+ from .parser import register
5
+
6
+ __all__ = ["register"]
@@ -0,0 +1,63 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Shared helpers for referrals commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+
8
+ from cli._output import to_data
9
+
10
+
11
+ def emit_code_human(result: object) -> None:
12
+ """Render a referral code result as human-readable key: value."""
13
+ data = to_data(result)
14
+ lines = [
15
+ f"code: {data.get('code')}",
16
+ f"is_default: {data.get('is_default')}",
17
+ ]
18
+ if data.get("label"):
19
+ lines.append(f"label: {data.get('label')}")
20
+ sys.stdout.write("\n".join(lines) + "\n")
21
+
22
+
23
+ def emit_link_human(result: object) -> None:
24
+ """Render a referral link result as human-readable key: value."""
25
+ data = to_data(result)
26
+ lines = [
27
+ f"referral_code: {data.get('referral_code')}",
28
+ f"link: {data.get('link')}",
29
+ ]
30
+ if data.get("course_id"):
31
+ lines.append(f"course_id: {data.get('course_id')}")
32
+ sys.stdout.write("\n".join(lines) + "\n")
33
+
34
+
35
+ def emit_stats_human(result: object) -> None:
36
+ """Render referral statistics as human-readable key: value."""
37
+ data = to_data(result)
38
+ lines = [
39
+ f"total_attributions: {data.get('total_attributions')}",
40
+ f"active_attributions: {data.get('active_attributions')}",
41
+ f"total_rewards_credited_cents: "
42
+ f"{data.get('total_rewards_credited_cents')}",
43
+ f"pending_rewards_cents: {data.get('pending_rewards_cents')}",
44
+ ]
45
+ sys.stdout.write("\n".join(lines) + "\n")
46
+
47
+
48
+ def emit_attributions_human(result: object) -> None:
49
+ """Render referral attributions as human-readable entries."""
50
+ items = to_data(result)
51
+ if not items:
52
+ sys.stdout.write("No referral attributions.\n")
53
+ else:
54
+ for item in items:
55
+ lines = [
56
+ f"id: {item.get('id')}",
57
+ f"referrer_user_id: {item.get('referrer_user_id')}",
58
+ f"referee_user_id: {item.get('referee_user_id')}",
59
+ f"status: {item.get('status')}",
60
+ f"signup_at: {item.get('signup_at')}",
61
+ f"expires_at: {item.get('expires_at')}",
62
+ ]
63
+ sys.stdout.write("\n".join(lines) + "\n---\n")
@@ -0,0 +1,100 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Handlers for referrals commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+
8
+ from cli._confirm import require_yes
9
+ from cli._context import make_client
10
+ from cli._errors import handle_error, validate_uuid_id
11
+ from cli._output import emit_json, to_data
12
+
13
+ from ._helpers import (
14
+ emit_attributions_human,
15
+ emit_code_human,
16
+ emit_link_human,
17
+ emit_stats_human,
18
+ )
19
+
20
+
21
+ def _run(
22
+ args: argparse.Namespace,
23
+ fn,
24
+ kind: str,
25
+ *,
26
+ json_output: bool,
27
+ render=None,
28
+ ):
29
+ """Call *fn* on the referrals resource and emit the result."""
30
+ config = resolve_config_from_args(args)
31
+ client = make_client(config)
32
+ try:
33
+ result = fn(client.v1.referrals)
34
+ if json_output:
35
+ emit_json(f"logion.referrals.{kind}", to_data(result))
36
+ elif render:
37
+ render(result)
38
+ except Exception as exc:
39
+ return handle_error(exc)
40
+ else:
41
+ return 0
42
+ finally:
43
+ client.close()
44
+
45
+
46
+ def handle_referrals_code(args: argparse.Namespace) -> int:
47
+ """Execute the referrals code command."""
48
+ return _run(
49
+ args,
50
+ lambda r: r.get_code(),
51
+ "code",
52
+ json_output=getattr(args, "json_output", False),
53
+ render=emit_code_human,
54
+ )
55
+
56
+
57
+ def handle_referrals_link(args: argparse.Namespace) -> int:
58
+ """Execute the referrals link command."""
59
+ bad_id = validate_uuid_id(args.course_id, "COURSE_ID")
60
+ if bad_id is not None:
61
+ return bad_id
62
+ refusal = require_yes(
63
+ args.yes,
64
+ "share this referral link",
65
+ )
66
+ if refusal is not None:
67
+ return refusal
68
+ return _run(
69
+ args,
70
+ lambda r: r.get_link(course_id=args.course_id),
71
+ "link",
72
+ json_output=getattr(args, "json_output", False),
73
+ render=emit_link_human,
74
+ )
75
+
76
+
77
+ def handle_referrals_stats(args: argparse.Namespace) -> int:
78
+ """Execute the referrals stats command."""
79
+ return _run(
80
+ args,
81
+ lambda r: r.get_stats(),
82
+ "stats",
83
+ json_output=getattr(args, "json_output", False),
84
+ render=emit_stats_human,
85
+ )
86
+
87
+
88
+ def handle_referrals_attributions(args: argparse.Namespace) -> int:
89
+ """Execute the referrals attributions command."""
90
+ return _run(
91
+ args,
92
+ lambda r: r.list_attributions(),
93
+ "attributions",
94
+ json_output=getattr(args, "json_output", False),
95
+ render=emit_attributions_human,
96
+ )
97
+
98
+
99
+ # Late import to avoid circulars at module scope
100
+ from cli._config import resolve_config_from_args # noqa: E402