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.
- cli/__init__.py +2 -0
- cli/_config.py +51 -0
- cli/_confirm.py +16 -0
- cli/_context.py +17 -0
- cli/_course_bundle.py +46 -0
- cli/_course_capabilities.py +580 -0
- cli/_credentials.py +104 -0
- cli/_errors.py +82 -0
- cli/_first_run.py +90 -0
- cli/_harness/__init__.py +68 -0
- cli/_harness/base.py +106 -0
- cli/_harness/claude_code.py +168 -0
- cli/_harness/codex.py +79 -0
- cli/_harness/custom.py +55 -0
- cli/_harness/hermes.py +93 -0
- cli/_harness/opencode.py +255 -0
- cli/_local_state.py +1053 -0
- cli/_options.py +36 -0
- cli/_output.py +47 -0
- cli/_parser.py +73 -0
- cli/_recall_calibration.py +90 -0
- cli/_recall_ranker.py +74 -0
- cli/_taxonomy.py +120 -0
- cli/_update_policy.py +152 -0
- cli/_utils.py +16 -0
- cli/_version.py +26 -0
- cli/commands/__init__.py +2 -0
- cli/commands/admin.py +535 -0
- cli/commands/bounties.py +490 -0
- cli/commands/course_reviews/__init__.py +6 -0
- cli/commands/course_reviews/_download_handler.py +104 -0
- cli/commands/course_reviews/_render.py +129 -0
- cli/commands/course_reviews/handlers.py +197 -0
- cli/commands/course_reviews/parser.py +93 -0
- cli/commands/courses/__init__.py +6 -0
- cli/commands/courses/_capability_render.py +183 -0
- cli/commands/courses/_cmd_help.py +18 -0
- cli/commands/courses/_purchase.py +76 -0
- cli/commands/courses/_review_helpers.py +93 -0
- cli/commands/courses/_taxonomy_data.py +173 -0
- cli/commands/courses/_upload_bundle_validation.py +28 -0
- cli/commands/courses/_uploads_push.py +243 -0
- cli/commands/courses/capabilities.py +250 -0
- cli/commands/courses/capability_frontmatter.py +150 -0
- cli/commands/courses/handlers.py +50 -0
- cli/commands/courses/mutations.py +217 -0
- cli/commands/courses/parser.py +66 -0
- cli/commands/courses/parser_capabilities.py +95 -0
- cli/commands/courses/parser_sections.py +239 -0
- cli/commands/courses/parser_uploads.py +84 -0
- cli/commands/courses/parser_utils.py +65 -0
- cli/commands/courses/publication.py +60 -0
- cli/commands/courses/report_usage.py +131 -0
- cli/commands/courses/reviews.py +237 -0
- cli/commands/courses/taxonomy_handler.py +61 -0
- cli/commands/courses/taxonomy_suggest.py +197 -0
- cli/commands/courses/uploads.py +142 -0
- cli/commands/courses/versions.py +65 -0
- cli/commands/credits/__init__.py +6 -0
- cli/commands/credits/_helpers.py +153 -0
- cli/commands/credits/handlers.py +218 -0
- cli/commands/credits/parser.py +115 -0
- cli/commands/docs/__init__.py +6 -0
- cli/commands/docs/handlers.py +137 -0
- cli/commands/docs/parser.py +27 -0
- cli/commands/health/__init__.py +6 -0
- cli/commands/health/handlers.py +26 -0
- cli/commands/health/parser.py +20 -0
- cli/commands/identity/__init__.py +6 -0
- cli/commands/identity/_autopost.py +97 -0
- cli/commands/identity/_closing_copy.py +89 -0
- cli/commands/identity/_companion.py +232 -0
- cli/commands/identity/_companion_source.py +135 -0
- cli/commands/identity/_harness_select.py +85 -0
- cli/commands/identity/_onboarding_helpers.py +168 -0
- cli/commands/identity/handlers.py +173 -0
- cli/commands/identity/onboarding.py +246 -0
- cli/commands/identity/parser.py +72 -0
- cli/commands/listings/__init__.py +6 -0
- cli/commands/listings/handlers.py +135 -0
- cli/commands/listings/parser.py +57 -0
- cli/commands/notifications/__init__.py +6 -0
- cli/commands/notifications/handlers.py +120 -0
- cli/commands/notifications/parser.py +49 -0
- cli/commands/payments/__init__.py +6 -0
- cli/commands/payments/_orders_helpers.py +114 -0
- cli/commands/payments/handlers.py +138 -0
- cli/commands/payments/parser.py +97 -0
- cli/commands/recall/__init__.py +7 -0
- cli/commands/recall/handlers.py +87 -0
- cli/commands/recall/parser.py +70 -0
- cli/commands/referrals/__init__.py +6 -0
- cli/commands/referrals/_helpers.py +63 -0
- cli/commands/referrals/handlers.py +100 -0
- cli/commands/referrals/parser.py +65 -0
- cli/commands/reports/__init__.py +6 -0
- cli/commands/reports/handlers.py +57 -0
- cli/commands/reports/parser.py +52 -0
- cli/commands/skills/__init__.py +7 -0
- cli/commands/skills/_agent_symlink.py +161 -0
- cli/commands/skills/_finalize.py +112 -0
- cli/commands/skills/_inspect_handler.py +218 -0
- cli/commands/skills/_install_helpers.py +186 -0
- cli/commands/skills/_query_handlers.py +83 -0
- cli/commands/skills/_search_handler.py +136 -0
- cli/commands/skills/_update_handler.py +110 -0
- cli/commands/skills/_verify_handler.py +109 -0
- cli/commands/skills/handlers.py +202 -0
- cli/commands/skills/parser.py +154 -0
- cli/commands/workspace.py +406 -0
- cli/docs/README.md +5 -0
- cli/docs/__init__.py +1 -0
- cli/docs/bounties-and-referrals.md +18 -0
- cli/docs/concepts.md +47 -0
- cli/docs/creating-courses.md +25 -0
- cli/docs/credits-and-purchases.md +30 -0
- cli/docs/credits-terms.md +23 -0
- cli/docs/getting-started.md +95 -0
- cli/docs/marketplace-loop.md +108 -0
- cli/docs/privacy.md +30 -0
- cli/docs/referral-terms.md +24 -0
- cli/docs/reviews.md +47 -0
- cli/docs/safety.md +28 -0
- cli/docs/terms.md +54 -0
- cli/main.py +84 -0
- cli/templates/__init__.py +2 -0
- cli/templates/course_capabilities.template.yaml +189 -0
- cli/templates/course_license_apache-2.0.template.txt +30 -0
- cli/templates/course_license_logion-standard-course-v1.template.txt +49 -0
- cli/templates/course_license_mit.template.txt +21 -0
- logion_cli-0.1.0.dist-info/METADATA +49 -0
- logion_cli-0.1.0.dist-info/RECORD +135 -0
- logion_cli-0.1.0.dist-info/WHEEL +4 -0
- logion_cli-0.1.0.dist-info/entry_points.txt +4 -0
- logion_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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,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,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
|