sedona-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.
sedona_cli/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Sedona CLI — export and upload AI-chat / terminal transcripts to company context.
2
+
3
+ Lives in the sedona-internal repo as a uv workspace member so the backend and
4
+ the CLI share one secret scrubber (``sedona_cli.redact``); published to PyPI as
5
+ ``sedona-cli``. Install on a laptop:
6
+
7
+ uv tool install sedona-cli
8
+
9
+ Then: ``sedona auth`` → ``sedona list`` → ``sedona send --recent 3``.
10
+ """
11
+
12
+ __version__ = "0.1.0"
sedona_cli/auth.py ADDED
@@ -0,0 +1,41 @@
1
+ """``sedona auth`` — email OTP → long-lived upload token.
2
+
3
+ The OTP exchange is proxied by the backend (``/transcripts/auth/start`` +
4
+ ``/verify``) so the CLI needs no Supabase configuration; proof of mailbox
5
+ control yields the same token the portal mint endpoint issues.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import httpx
11
+
12
+ from sedona_cli import config
13
+
14
+
15
+ def run_auth(url: str | None = None) -> int:
16
+ base = (url or config.base_url()).rstrip("/")
17
+ email = input("Work email: ").strip().lower()
18
+ if not email or "@" not in email:
19
+ print("That doesn't look like an email address.")
20
+ return 1
21
+
22
+ with httpx.Client(timeout=30) as client:
23
+ resp = client.post(f"{base}/transcripts/auth/start", json={"email": email})
24
+ if resp.status_code != 200:
25
+ print(f"Could not send code: {resp.json().get('detail', resp.text)}")
26
+ return 1
27
+ print(f"Code sent to {email}.")
28
+
29
+ code = input("6-digit code: ").strip()
30
+ resp = client.post(
31
+ f"{base}/transcripts/auth/verify", json={"email": email, "code": code}
32
+ )
33
+ if resp.status_code != 200:
34
+ print(f"Verification failed: {resp.json().get('detail', resp.text)}")
35
+ return 1
36
+ token = resp.json()["token"]
37
+
38
+ config.save(url=base, token=token)
39
+ print("Authenticated — token saved to ~/.config/sedona/config.json.")
40
+ print("Try: sedona list then sedona send --recent 1")
41
+ return 0
sedona_cli/config.py ADDED
@@ -0,0 +1,34 @@
1
+ """CLI config: Sedona base URL + upload token in ~/.config/sedona/config.json."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ DEFAULT_URL = "https://sedona-internal-production.up.railway.app"
10
+
11
+ _CONFIG_DIR = Path(os.environ.get("SEDONA_CONFIG_DIR", "~/.config/sedona")).expanduser()
12
+ _CONFIG_PATH = _CONFIG_DIR / "config.json"
13
+
14
+
15
+ def load() -> dict:
16
+ try:
17
+ return json.loads(_CONFIG_PATH.read_text())
18
+ except (OSError, json.JSONDecodeError):
19
+ return {}
20
+
21
+
22
+ def save(**updates) -> None:
23
+ cfg = {**load(), **updates}
24
+ _CONFIG_DIR.mkdir(parents=True, exist_ok=True)
25
+ _CONFIG_PATH.write_text(json.dumps(cfg, indent=2) + "\n")
26
+ _CONFIG_PATH.chmod(0o600)
27
+
28
+
29
+ def base_url() -> str:
30
+ return (os.environ.get("SEDONA_URL") or load().get("url") or DEFAULT_URL).rstrip("/")
31
+
32
+
33
+ def token() -> str | None:
34
+ return os.environ.get("SEDONA_TRANSCRIPT_TOKEN") or load().get("token")
sedona_cli/discover.py ADDED
@@ -0,0 +1,91 @@
1
+ """Find local AI-chat context worth uploading.
2
+
3
+ Claude Code sessions live at ``~/.claude/projects/<project-slug>/<uuid>.jsonl``;
4
+ the session title is on an ``ai-title`` (newer) or ``summary`` (older) line.
5
+ Claude.ai / ChatGPT data exports land in ``~/Downloads`` as
6
+ ``conversations.json`` (possibly inside the export zip the user expanded).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+
17
+ @dataclass
18
+ class SessionInfo:
19
+ path: Path
20
+ project: str
21
+ title: str
22
+ mtime: float
23
+ size: int
24
+
25
+
26
+ def _claude_dir() -> Path:
27
+ return Path(os.environ.get("CLAUDE_CONFIG_DIR", "~/.claude")).expanduser()
28
+
29
+
30
+ def session_title(path: Path) -> str:
31
+ """Best-effort title: last ai-title/summary line, else first user text."""
32
+ title = ""
33
+ first_user = ""
34
+ try:
35
+ with open(path, encoding="utf-8", errors="replace") as f:
36
+ for line in f:
37
+ if '"ai-title"' not in line and '"summary"' not in line and (
38
+ first_user or '"user"' not in line
39
+ ):
40
+ continue
41
+ try:
42
+ obj = json.loads(line)
43
+ except json.JSONDecodeError:
44
+ continue
45
+ if obj.get("type") == "ai-title" and obj.get("aiTitle"):
46
+ title = obj["aiTitle"]
47
+ elif obj.get("type") == "summary" and obj.get("summary"):
48
+ title = obj["summary"]
49
+ elif not first_user and obj.get("type") == "user":
50
+ content = (obj.get("message") or {}).get("content")
51
+ if isinstance(content, str) and content.strip():
52
+ first_user = content.strip().splitlines()[0][:80]
53
+ except OSError:
54
+ pass
55
+ return title or first_user or path.stem
56
+
57
+
58
+ def find_sessions(project: str | None = None, limit: int = 20) -> list[SessionInfo]:
59
+ """Claude Code sessions, newest first."""
60
+ root = _claude_dir() / "projects"
61
+ if not root.is_dir():
62
+ return []
63
+ paths = [
64
+ p
65
+ for p in root.glob("*/*.jsonl")
66
+ if not project or project in p.parent.name
67
+ ]
68
+ paths.sort(key=lambda p: p.stat().st_mtime, reverse=True)
69
+ out = []
70
+ for p in paths[:limit]:
71
+ st = p.stat()
72
+ out.append(
73
+ SessionInfo(
74
+ path=p,
75
+ project=p.parent.name.lstrip("-").replace("-", "/"),
76
+ title=session_title(p),
77
+ mtime=st.st_mtime,
78
+ size=st.st_size,
79
+ )
80
+ )
81
+ return out
82
+
83
+
84
+ def find_chat_exports() -> list[Path]:
85
+ """conversations.json files (Claude.ai / ChatGPT data exports) in ~/Downloads."""
86
+ downloads = Path("~/Downloads").expanduser()
87
+ if not downloads.is_dir():
88
+ return []
89
+ hits = list(downloads.glob("conversations.json"))
90
+ hits += [p for p in downloads.glob("*/conversations.json")]
91
+ return sorted(hits, key=lambda p: p.stat().st_mtime, reverse=True)
sedona_cli/main.py ADDED
@@ -0,0 +1,169 @@
1
+ """``sedona`` — upload AI-chat / terminal transcripts into company context.
2
+
3
+ Commands:
4
+ sedona auth authenticate via email OTP
5
+ sedona list show recent Claude Code sessions + exports
6
+ sedona send --recent N upload the N most recent sessions
7
+ sedona send <path> [<path>...] upload specific files
8
+ cmd | sedona send --stdin upload raw terminal output
9
+ sedona init-skill install the local Claude Code skill
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import sys
16
+ import time
17
+ from pathlib import Path
18
+
19
+ import httpx
20
+
21
+ from sedona_cli import config
22
+ from sedona_cli.auth import run_auth
23
+ from sedona_cli.discover import find_chat_exports, find_sessions
24
+ from sedona_cli.redact import redact_secrets
25
+
26
+ _MAX_UPLOAD_BYTES = 25 * 1024 * 1024
27
+
28
+
29
+ def _age(mtime: float) -> str:
30
+ mins = max(0, int((time.time() - mtime) / 60))
31
+ if mins < 60:
32
+ return f"{mins}m ago"
33
+ if mins < 60 * 24:
34
+ return f"{mins // 60}h ago"
35
+ return f"{mins // (60 * 24)}d ago"
36
+
37
+
38
+ def cmd_list(args) -> int:
39
+ sessions = find_sessions(project=args.project)
40
+ if sessions:
41
+ print("Recent Claude Code sessions:")
42
+ for s in sessions:
43
+ print(f" {_age(s.mtime):>8} {s.size // 1024:>6} KB [{s.project}] {s.title}")
44
+ else:
45
+ print("No Claude Code sessions found under ~/.claude/projects.")
46
+ exports = find_chat_exports()
47
+ if exports:
48
+ print("\nChat exports in ~/Downloads:")
49
+ for p in exports:
50
+ print(f" {p}")
51
+ print("\nUpload with: sedona send --recent 1 or sedona send <path>")
52
+ return 0
53
+
54
+
55
+ def _upload(client: httpx.Client, base: str, token: str, path: Path | None, content: str) -> bool:
56
+ name = path.name if path else "terminal-stdin.txt"
57
+ clean, redactions = redact_secrets(content)
58
+ if len(clean.encode()) > _MAX_UPLOAD_BYTES:
59
+ print(f" ✗ {name}: exceeds 25 MB, skipping")
60
+ return False
61
+ resp = client.post(
62
+ f"{base}/transcripts/upload",
63
+ json={"filename": name, "content": clean},
64
+ headers={"X-Transcript-Token": token},
65
+ timeout=300,
66
+ )
67
+ if resp.status_code != 200:
68
+ try:
69
+ detail = resp.json().get("detail", resp.text)
70
+ except ValueError:
71
+ detail = resp.text
72
+ print(f" ✗ {name}: {resp.status_code} {detail}")
73
+ return False
74
+ body = resp.json()
75
+ if body.get("status") == "queued":
76
+ print(
77
+ f" ⧖ {name}: {body['conversations']} conversations queued "
78
+ f"(ingesting {body['ingesting']}, skipped {body['skipped']}), redactions={redactions}"
79
+ )
80
+ return True
81
+ for doc in body.get("documents", []):
82
+ print(
83
+ f" ✓ {doc['title']} [{doc['format']}, visibility={doc['visibility']}, "
84
+ f"chunks={doc['chunks']}, v{doc['version']}]"
85
+ )
86
+ if redactions or body.get("redactions"):
87
+ print(f" redacted locally={redactions}, server-side={body.get('redactions', 0)}")
88
+ return True
89
+
90
+
91
+ def cmd_send(args) -> int:
92
+ token = config.token()
93
+ if not token:
94
+ print("Not authenticated — run `sedona auth` first.")
95
+ return 1
96
+ base = config.base_url()
97
+
98
+ targets: list[Path] = [Path(p).expanduser() for p in args.paths]
99
+ if args.recent:
100
+ sessions = find_sessions(project=args.project, limit=args.recent)
101
+ if not sessions:
102
+ print("No sessions found to send.")
103
+ return 1
104
+ targets += [s.path for s in sessions]
105
+
106
+ ok = True
107
+ with httpx.Client(timeout=300) as client:
108
+ if args.stdin:
109
+ content = sys.stdin.read()
110
+ if content.strip():
111
+ ok &= _upload(client, base, token, None, content)
112
+ else:
113
+ print("Nothing on stdin.")
114
+ ok = False
115
+ for path in targets:
116
+ if not path.is_file():
117
+ print(f" ✗ {path}: not a file")
118
+ ok = False
119
+ continue
120
+ ok &= _upload(client, base, token, path, path.read_text(errors="replace"))
121
+
122
+ if not targets and not args.stdin:
123
+ print("Nothing to send — pass paths, --recent N, or --stdin.")
124
+ return 1
125
+ return 0 if ok else 1
126
+
127
+
128
+ def cmd_init_skill(args) -> int:
129
+ template = Path(__file__).parent / "skill_template.md"
130
+ dest = Path("~/.claude/skills/sedona-upload/SKILL.md").expanduser()
131
+ dest.parent.mkdir(parents=True, exist_ok=True)
132
+ dest.write_text(template.read_text())
133
+ print(f"Installed skill → {dest}")
134
+ print('Your local Claude Code can now act on "share this session with Sedona".')
135
+ return 0
136
+
137
+
138
+ def main(argv: list[str] | None = None) -> int:
139
+ parser = argparse.ArgumentParser(prog="sedona", description=__doc__.split("\n")[0])
140
+ sub = parser.add_subparsers(dest="command", required=True)
141
+
142
+ p_auth = sub.add_parser("auth", help="authenticate via email OTP")
143
+ p_auth.add_argument("--url", help="Sedona base URL (default: saved or SEDONA_URL)")
144
+
145
+ p_list = sub.add_parser("list", help="show recent sessions and chat exports")
146
+ p_list.add_argument("--project", help="filter sessions by project slug substring")
147
+
148
+ p_send = sub.add_parser("send", help="scrub and upload transcripts")
149
+ p_send.add_argument("paths", nargs="*", help="transcript files to upload")
150
+ p_send.add_argument("--recent", type=int, metavar="N", help="send the N most recent sessions")
151
+ p_send.add_argument("--project", help="with --recent: filter by project slug substring")
152
+ p_send.add_argument("--stdin", action="store_true", help="read a terminal capture from stdin")
153
+
154
+ sub.add_parser("init-skill", help="install the Claude Code skill into ~/.claude/skills")
155
+
156
+ args = parser.parse_args(argv)
157
+ if args.command == "auth":
158
+ return run_auth(args.url)
159
+ if args.command == "list":
160
+ return cmd_list(args)
161
+ if args.command == "send":
162
+ return cmd_send(args)
163
+ if args.command == "init-skill":
164
+ return cmd_init_skill(args)
165
+ return 2
166
+
167
+
168
+ if __name__ == "__main__":
169
+ raise SystemExit(main())
sedona_cli/redact.py ADDED
@@ -0,0 +1,92 @@
1
+ """Secret scrubbing for transcripts — the single source of truth.
2
+
3
+ Used client-side by ``sedona send`` (secrets never leave the laptop) and
4
+ server-side by the transcript ingest path as a backstop (re-exported as
5
+ ``src.lib.redact``). Stdlib-only by design so the CLI stays dependency-light.
6
+
7
+ Redaction is fail-closed: a false positive loses one token from a transcript,
8
+ a false negative puts a credential in the company graph. The entropy guard
9
+ exists only to keep *hex* artifacts (git SHAs, UUIDs, document hashes) alive —
10
+ hex tops out at 4 bits/char, random base64 sits well above it.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import math
16
+ import re
17
+
18
+ __all__ = ["redact_secrets"]
19
+
20
+ _MASK = "[REDACTED:{kind}]"
21
+
22
+ # Ordered: specific vendor formats before generic patterns, so the mask kind
23
+ # stays informative and the generic passes never see already-masked text.
24
+ _PATTERNS: list[tuple[str, re.Pattern[str]]] = [
25
+ (
26
+ "pem",
27
+ re.compile(
28
+ r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----",
29
+ re.DOTALL,
30
+ ),
31
+ ),
32
+ ("aws-key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
33
+ ("anthropic", re.compile(r"\bsk-ant-[A-Za-z0-9_-]{20,}")),
34
+ ("api-key", re.compile(r"\bsk-[A-Za-z0-9_-]{32,}")),
35
+ ("github", re.compile(r"\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})")),
36
+ ("slack", re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{10,}")),
37
+ ("jwt", re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}")),
38
+ ("bearer", re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._~+/=-]{20,}")),
39
+ ]
40
+
41
+ # `KEY=value` / `key: value` assignments — keep the key name, mask the value.
42
+ _ASSIGNMENT = re.compile(
43
+ r"(?i)\b([A-Za-z0-9_-]*(?:api[_-]?key|secret|token|password|passwd|credential)s?)"
44
+ r"(\s*[=:]\s*)(['\"]?)([^\s'\"\[\]]{16,})\3"
45
+ )
46
+
47
+ # Bare high-entropy tokens (base64-ish, 32+ chars). Requires at least one
48
+ # non-hex character so hex artifacts can never match regardless of entropy.
49
+ _TOKEN = re.compile(r"\b(?=[A-Za-z0-9+/_-]*[G-Zg-z+/_-])[A-Za-z0-9+/_-]{32,}\b")
50
+
51
+ _ENTROPY_THRESHOLD = 4.0
52
+
53
+
54
+ def _shannon_entropy(s: str) -> float:
55
+ counts: dict[str, int] = {}
56
+ for ch in s:
57
+ counts[ch] = counts.get(ch, 0) + 1
58
+ n = len(s)
59
+ return -sum(c / n * math.log2(c / n) for c in counts.values())
60
+
61
+
62
+ def redact_secrets(text: str) -> tuple[str, int]:
63
+ """Mask credentials in ``text``. Returns ``(clean_text, num_redactions)``."""
64
+ count = 0
65
+
66
+ def _sub(pattern: re.Pattern[str], repl, s: str) -> str:
67
+ nonlocal count
68
+ out, n = pattern.subn(repl, s)
69
+ count += n
70
+ return out
71
+
72
+ for kind, pattern in _PATTERNS:
73
+ text = _sub(pattern, _MASK.format(kind=kind), text)
74
+
75
+ text = _sub(
76
+ _ASSIGNMENT,
77
+ lambda m: f"{m.group(1)}{m.group(2)}{_MASK.format(kind='value')}",
78
+ text,
79
+ )
80
+
81
+ def _entropy_repl(m: re.Match[str]) -> str:
82
+ token = m.group(0)
83
+ if re.fullmatch(r"[0-9a-fA-F-]+", token): # UUIDs, SHAs, doc hashes
84
+ return token
85
+ if "REDACTED" in token or _shannon_entropy(token) <= _ENTROPY_THRESHOLD:
86
+ return token
87
+ nonlocal count
88
+ count += 1
89
+ return _MASK.format(kind="token")
90
+
91
+ out, _ = _TOKEN.subn(_entropy_repl, text)
92
+ return out, count
@@ -0,0 +1,29 @@
1
+ # Share this session with Sedona
2
+
3
+ Use this skill when the user asks to "share this session with Sedona", "upload
4
+ this conversation to company context", "send this thread to Sedona", or similar.
5
+
6
+ Sedona is the company's knowledge agent. Uploading a session makes its content
7
+ searchable company context (secrets are scrubbed locally before upload, and the
8
+ server classifies visibility — sensitive sessions are restricted automatically).
9
+
10
+ ## Steps
11
+
12
+ 1. Check the CLI is set up: `sedona list` should print recent sessions. If the
13
+ command is missing, install it: `uv tool install sedona-cli`. If it fails
14
+ with a missing-token error, run `sedona auth` first (interactive — ask the
15
+ user to run it themselves in a terminal).
16
+ 2. To upload the most recent session(s) of this project:
17
+ `sedona send --recent 1` (or `--recent N` for the last N).
18
+ To upload a specific session file: `sedona send <path>`.
19
+ For raw terminal scrollback: pipe it — `history | sedona send --stdin`.
20
+ 3. Report the result to the user: each uploaded document's title, visibility
21
+ tier, and redaction count are printed by the CLI.
22
+
23
+ ## Notes
24
+
25
+ - The currently active session's file is still being written; prefer uploading
26
+ after the work wraps up, or warn the user the upload is a snapshot.
27
+ - Claude.ai / ChatGPT exports (`conversations.json`) can also be sent:
28
+ `sedona send ~/Downloads/conversations.json`. Large exports are capped
29
+ server-side (~25 conversations per file); suggest the user trim if needed.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: sedona-cli
3
+ Version: 0.1.0
4
+ Summary: Export your AI chats — Claude Code, Claude.ai/Cowork, ChatGPT — and terminal sessions into your company's context.
5
+ Project-URL: Homepage, https://github.com/Sedona-Health/sedona-internal
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: chatgpt,claude,claude-code,context,export,knowledge-base,transcripts
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx>=0.27
20
+ Description-Content-Type: text/markdown
21
+
22
+ # sedona-cli
23
+
24
+ Your AI chats hold a surprising amount of company context — decisions, debugging
25
+ trails, design discussions, institutional knowledge that never makes it into a
26
+ doc. `sedona` ships them into Sedona's company knowledge graph, where they
27
+ become searchable context for everyone (with secrets scrubbed and sensitive
28
+ sessions automatically restricted).
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ uv tool install sedona-cli # or: pipx install sedona-cli / pip install sedona-cli
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```bash
39
+ sedona auth # email OTP — requires a company employee email
40
+ sedona list # recent Claude Code sessions + chat exports it found
41
+ sedona send --recent 3 # scrub + upload your 3 most recent sessions
42
+ sedona send ~/Downloads/conversations.json # a Claude.ai / ChatGPT export
43
+ history | sedona send --stdin # raw terminal scrollback
44
+ sedona init-skill # let your local Claude Code do this on request
45
+ ```
46
+
47
+ After `sedona init-skill`, you can just tell Claude Code *"share this session
48
+ with Sedona"* and it handles the upload.
49
+
50
+ ## What it can export
51
+
52
+ | Source | How |
53
+ |---|---|
54
+ | **Claude Code** sessions | Read directly from `~/.claude/projects/` — `sedona send --recent N` |
55
+ | **Claude.ai / Cowork** chats | Request a data export in claude.ai settings, then `sedona send conversations.json` |
56
+ | **ChatGPT** chats | Request a data export in ChatGPT settings, then `sedona send conversations.json` |
57
+ | **Terminal** sessions | Pipe anything: `tmux capture-pane -p \| sedona send --stdin` |
58
+
59
+ ## Privacy
60
+
61
+ - **Secrets never leave your machine**: API keys, tokens, JWTs, and private
62
+ keys are redacted locally before upload (and the server scrubs again as a
63
+ backstop).
64
+ - The server classifies each conversation's visibility **fail-closed** —
65
+ sensitive content is restricted to you or admins, not shared company-wide.
66
+ - Tool output in coding sessions is truncated; assistant thinking blocks are
67
+ dropped entirely.
68
+ - Authentication requires a verified company employee email; the tool does
69
+ nothing useful outside the company.
70
+
71
+ ## License
72
+
73
+ MIT (this CLI only).
@@ -0,0 +1,12 @@
1
+ sedona_cli/__init__.py,sha256=7xbqYJS0CEjCJAA0KBg2yGI8MC5F6z17yaOR4u3v5bU,417
2
+ sedona_cli/auth.py,sha256=WKsMgRRqQwNdwSYPZrpSMrE0KKBOIGV53cThGhixNjk,1464
3
+ sedona_cli/config.py,sha256=hx2S2GnEthJQeQxK_I2lV-1rEIbbupuN3ttjqAyqtpM,937
4
+ sedona_cli/discover.py,sha256=o2L2bS4UcPcuEWo70NUU-yQcSk9fLoe609LmEur4gEg,3008
5
+ sedona_cli/main.py,sha256=mJd7xQSl-9sRMBuA_BXL6w7G1h5N9g_02spridFjfxU,6100
6
+ sedona_cli/redact.py,sha256=YT6JvRk7sV8YqTwGaSf143CLpEhSQXxdAAhQblvH7rU,3272
7
+ sedona_cli/skill_template.md,sha256=fM1e_D5lNfIlVThWIVCbIjSJCUkvU9Tq9P6H4V1xcgM,1496
8
+ sedona_cli-0.1.0.dist-info/METADATA,sha256=Q5LYXBgnuXixVmpE1SzMGfXG-bKZ1xvtXOKHTH3vBAY,2993
9
+ sedona_cli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ sedona_cli-0.1.0.dist-info/entry_points.txt,sha256=-anWIsV_zWUpR59OrOtV1SMNARE5jRzQxv9N1t9NvMs,48
11
+ sedona_cli-0.1.0.dist-info/licenses/LICENSE,sha256=eY6CBcKZC67XVZXae1ZIJPhVJdI7gFlpkS8twRCDN1Q,1070
12
+ sedona_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sedona = sedona_cli.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sedona Health
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.