substack-cli 0.9.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.
@@ -0,0 +1,106 @@
1
+ """``substack-cli whoami`` — the smallest identity probe.
2
+
3
+ Reports the agent's identity as declared in ``culture.yaml``: its nick
4
+ (``suffix``), the backend it runs on, and the served model (if any) — plus the
5
+ package version. Read-only; touches nothing but its own ``culture.yaml``.
6
+
7
+ When you clone this template, rename the package and update ``culture.yaml`` —
8
+ ``whoami`` then reflects your new agent's identity with no code change.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ from pathlib import Path
15
+
16
+ from substack_cli import __version__
17
+ from substack_cli.cli._output import emit_result
18
+
19
+ _FALLBACK_NICK = "substack-cli"
20
+
21
+
22
+ def find_culture_yaml() -> Path | None:
23
+ """Locate this agent's own ``culture.yaml`` by walking up from this module.
24
+
25
+ The identity must be the agent's own, not whatever ``culture.yaml`` happens
26
+ to sit in the caller's current working directory. In an editable / source
27
+ install, walking up from ``__file__`` finds the repo root; in a wheel
28
+ install no ``culture.yaml`` ships alongside the package and the caller falls
29
+ back to the literal defaults.
30
+ """
31
+ here = Path(__file__).resolve()
32
+ for parent in here.parents:
33
+ candidate = parent / "culture.yaml"
34
+ if candidate.is_file():
35
+ return candidate
36
+ return None
37
+
38
+
39
+ def read_agent_fields() -> dict[str, str]:
40
+ """Return ``suffix``/``backend``/``model`` from the first agent block.
41
+
42
+ Parsed without a YAML dependency to keep the runtime deps empty. Reads
43
+ top-level ``key: value`` lines within the first agent entry; anything
44
+ fancier than the documented shape falls back to the defaults below.
45
+ """
46
+ fields = {"nick": _FALLBACK_NICK, "backend": "unknown", "model": "unknown"}
47
+ cfg = find_culture_yaml()
48
+ if cfg is None:
49
+ return fields
50
+ try:
51
+ text = cfg.read_text(encoding="utf-8")
52
+ except OSError:
53
+ return fields
54
+ seen_agent = False
55
+ for line in text.splitlines():
56
+ stripped = line.strip()
57
+ if stripped.startswith(("- suffix:", "suffix:")):
58
+ if seen_agent: # second agent block — stop at the first
59
+ break
60
+ seen_agent = True
61
+ fields["nick"] = _scalar(stripped, "suffix")
62
+ elif seen_agent and stripped.startswith("backend:"):
63
+ fields["backend"] = _scalar(stripped, "backend")
64
+ elif seen_agent and stripped.startswith("model:"):
65
+ fields["model"] = _scalar(stripped, "model")
66
+ return fields
67
+
68
+
69
+ def _scalar(line: str, key: str) -> str:
70
+ """Extract the scalar after ``key:`` from a ``culture.yaml`` line."""
71
+ _, _, value = line.partition(f"{key}:")
72
+ return value.strip().strip("'\"") or "unknown"
73
+
74
+
75
+ def report() -> dict[str, object]:
76
+ fields = read_agent_fields()
77
+ return {
78
+ "nick": fields["nick"],
79
+ "version": __version__,
80
+ "backend": fields["backend"],
81
+ "model": fields["model"],
82
+ }
83
+
84
+
85
+ def cmd_whoami(args: argparse.Namespace) -> None:
86
+ identity = report()
87
+ json_mode = bool(getattr(args, "json", False))
88
+ if json_mode:
89
+ emit_result(identity, json_mode=True)
90
+ return
91
+ text = (
92
+ f"nick: {identity['nick']}\n"
93
+ f"version: {identity['version']}\n"
94
+ f"backend: {identity['backend']}\n"
95
+ f"model: {identity['model']}"
96
+ )
97
+ emit_result(text, json_mode=False)
98
+
99
+
100
+ def register(sub: argparse._SubParsersAction) -> None:
101
+ p = sub.add_parser(
102
+ "whoami",
103
+ help="Report this agent's nick, version, backend, and served model.",
104
+ )
105
+ p.add_argument("--json", action="store_true", help="Emit structured JSON.")
106
+ p.set_defaults(func=cmd_whoami)
@@ -0,0 +1,42 @@
1
+ """CliError and exit-code policy (stable-contract).
2
+
3
+ Every failure inside substack-cli raises :class:`CliError`. The
4
+ top-level ``main()`` catches it, formats via :mod:`substack_cli.cli._output`,
5
+ and exits with :attr:`CliError.code`. This guarantees:
6
+
7
+ * no Python traceback leaks to stderr (the agent-first error contract);
8
+ * every error has a structured shape ``{code, message, remediation}``;
9
+ * the exit-code policy is centralised in one place.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+
16
+ # Exit-code policy. Documented in ``substack-cli learn`` output.
17
+ # 0 = success
18
+ # 1 = user-input error (bad flag, missing required arg, unknown path)
19
+ # 2 = environment / setup error (tool not installed, file unreadable)
20
+ # 3+ = reserved for future categorisation
21
+ EXIT_SUCCESS = 0
22
+ EXIT_USER_ERROR = 1
23
+ EXIT_ENV_ERROR = 2
24
+
25
+
26
+ @dataclass
27
+ class CliError(Exception):
28
+ """Structured error raised within the CLI; carries a remediation hint for agents."""
29
+
30
+ code: int
31
+ message: str
32
+ remediation: str = ""
33
+
34
+ def __post_init__(self) -> None:
35
+ super().__init__(self.message)
36
+
37
+ def to_dict(self) -> dict[str, object]:
38
+ return {
39
+ "code": self.code,
40
+ "message": self.message,
41
+ "remediation": self.remediation,
42
+ }
@@ -0,0 +1,53 @@
1
+ """stdout / stderr helpers with a strict split (stable-contract).
2
+
3
+ Rule: **results go to stdout, diagnostics and errors go to stderr.** Agents
4
+ parsing output can rely on this invariant. JSON mode routes structured
5
+ payloads to the same streams — never mixes them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from typing import Any, TextIO
13
+
14
+ from substack_cli.cli._errors import CliError
15
+
16
+
17
+ def emit_result(data: Any, *, json_mode: bool, stream: TextIO | None = None) -> None:
18
+ """Write a command result to stdout (or ``stream``)."""
19
+ s = stream if stream is not None else sys.stdout
20
+ if json_mode:
21
+ json.dump(data, s, ensure_ascii=False)
22
+ s.write("\n")
23
+ return
24
+ text = data if isinstance(data, str) else str(data)
25
+ s.write(text)
26
+ if not text.endswith("\n"):
27
+ s.write("\n")
28
+
29
+
30
+ def emit_error(err: CliError, *, json_mode: bool, stream: TextIO | None = None) -> None:
31
+ """Write a :class:`CliError` to stderr.
32
+
33
+ Text mode renders as two lines when a remediation is present::
34
+
35
+ error: <message>
36
+ hint: <remediation>
37
+
38
+ The ``hint:`` prefix is required by the agent-first error rubric.
39
+ """
40
+ s = stream if stream is not None else sys.stderr
41
+ if json_mode:
42
+ json.dump(err.to_dict(), s, ensure_ascii=False)
43
+ s.write("\n")
44
+ return
45
+ s.write(f"error: {err.message}\n")
46
+ if err.remediation:
47
+ s.write(f"hint: {err.remediation}\n")
48
+
49
+
50
+ def emit_diagnostic(message: str, *, stream: TextIO | None = None) -> None:
51
+ """Write a human diagnostic (progress, summary) to stderr."""
52
+ s = stream if stream is not None else sys.stderr
53
+ s.write(message if message.endswith("\n") else message + "\n")
@@ -0,0 +1,24 @@
1
+ """Explain catalog — markdown keyed by command-path tuples (stable-contract).
2
+
3
+ Every noun/verb registered in the CLI should have a catalog entry.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from substack_cli.cli._errors import EXIT_USER_ERROR, CliError
9
+ from substack_cli.explain.catalog import ENTRIES
10
+
11
+
12
+ def resolve(path: tuple[str, ...]) -> str:
13
+ if path in ENTRIES:
14
+ return ENTRIES[path]
15
+ display = " ".join(path) if path else "<root>"
16
+ raise CliError(
17
+ code=EXIT_USER_ERROR,
18
+ message=f"no explain entry for: {display}",
19
+ remediation="list entries with: substack-cli explain substack-cli",
20
+ )
21
+
22
+
23
+ def known_paths() -> list[tuple[str, ...]]:
24
+ return list(ENTRIES.keys())
@@ -0,0 +1,136 @@
1
+ """Markdown catalog for ``substack-cli explain <path>``.
2
+
3
+ Each entry is verbatim markdown. Keys are command-path tuples. The empty tuple
4
+ and ``("substack-cli",)`` both resolve to the root entry.
5
+
6
+ Keep bodies self-contained: an agent reading one entry should get enough
7
+ context without chaining reads.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ _ROOT = """\
13
+ # substack-cli
14
+
15
+ A clonable template for AgentCulture mesh agents. It carries an agent-first CLI
16
+ (cited from the teken `python-cli` reference), a mesh identity (`culture.yaml` +
17
+ `CLAUDE.md`), the canonical guildmaster skill kit under `.claude/skills/`, and a
18
+ buildable/deployable package baseline. Clone it, rename the package, edit
19
+ `culture.yaml`, and you have a new agent.
20
+
21
+ ## Verbs
22
+
23
+ - `substack-cli whoami` — identity probe from `culture.yaml`.
24
+ - `substack-cli learn` — structured self-teaching prompt.
25
+ - `substack-cli explain <path>` — markdown docs for any noun/verb.
26
+ - `substack-cli overview` — descriptive snapshot of the agent.
27
+ - `substack-cli doctor` — check the agent-identity invariants.
28
+ - `substack-cli cli overview` — describe the CLI surface.
29
+
30
+ ## Exit-code policy
31
+
32
+ - `0` success
33
+ - `1` user-input error
34
+ - `2` environment / setup error
35
+ - `3+` reserved
36
+
37
+ ## See also
38
+
39
+ - `substack-cli explain whoami`
40
+ - `substack-cli explain doctor`
41
+ """
42
+
43
+ _WHOAMI = """\
44
+ # substack-cli whoami
45
+
46
+ Reports the agent's identity from `culture.yaml`: nick (`suffix`), backend,
47
+ served model, and the package version. Read-only.
48
+
49
+ ## Usage
50
+
51
+ substack-cli whoami
52
+ substack-cli whoami --json
53
+ """
54
+
55
+ _LEARN = """\
56
+ # substack-cli learn
57
+
58
+ Prints a structured self-teaching prompt covering purpose, command map,
59
+ exit-code policy, `--json` support, and the `explain` pointer.
60
+
61
+ ## Usage
62
+
63
+ substack-cli learn
64
+ substack-cli learn --json
65
+ """
66
+
67
+ _EXPLAIN = """\
68
+ # substack-cli explain <path>
69
+
70
+ Prints markdown documentation for any noun/verb path. Unlike `--help` (terse,
71
+ positional), `explain` is global and addressable by path.
72
+
73
+ ## Usage
74
+
75
+ substack-cli explain substack-cli
76
+ substack-cli explain whoami
77
+ substack-cli explain --json <path>
78
+ """
79
+
80
+ _OVERVIEW = """\
81
+ # substack-cli overview
82
+
83
+ Read-only descriptive snapshot of the agent: identity (from `culture.yaml`), the
84
+ verb surface, and the sibling-pattern artifacts the template carries. Accepts an
85
+ ignored `target` so a stray path never hard-fails.
86
+
87
+ ## Usage
88
+
89
+ substack-cli overview
90
+ substack-cli overview --json
91
+ """
92
+
93
+ _DOCTOR = """\
94
+ # substack-cli doctor
95
+
96
+ Checks the agent-identity invariants `steward doctor` verifies:
97
+ prompt-file-present and backend-consistency (`claude` → `CLAUDE.md`), plus a
98
+ skills-present check. Exits 1 when unhealthy.
99
+
100
+ prompt-file-present requires the *resident* prompt the declared backend
101
+ actually reads. Other harness prompt files recognized under the same backend
102
+ name (`AGENTS.override.md`, `.pi/SYSTEM.md`, `QWEN.md`) belong to
103
+ interactively available harnesses the mesh daemon never loads; they are
104
+ reported by the informational harness-prompts check and never substituted.
105
+
106
+ ## Usage
107
+
108
+ substack-cli doctor
109
+ substack-cli doctor --json
110
+ """
111
+
112
+ _CLI = """\
113
+ # substack-cli cli
114
+
115
+ Noun group for CLI-surface introspection. `cli overview` describes the CLI
116
+ itself (distinct from the global `overview`, which describes the agent).
117
+
118
+ ## Usage
119
+
120
+ substack-cli cli overview
121
+ substack-cli cli overview --json
122
+ """
123
+
124
+
125
+ ENTRIES: dict[tuple[str, ...], str] = {
126
+ (): _ROOT,
127
+ ("substack-cli",): _ROOT,
128
+ ("substack",): _ROOT,
129
+ ("whoami",): _WHOAMI,
130
+ ("learn",): _LEARN,
131
+ ("explain",): _EXPLAIN,
132
+ ("overview",): _OVERVIEW,
133
+ ("doctor",): _DOCTOR,
134
+ ("cli",): _CLI,
135
+ ("cli", "overview"): _CLI,
136
+ }
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.5
2
+ Name: substack-cli
3
+ Version: 0.9.0
4
+ Summary: Agent-first CLI to manage a Substack publication and account: publish and schedule posts, read posts and comments, run audience and post statistics, and manage subscribers. Unofficial community tool, not affiliated with Substack.
5
+ Project-URL: Homepage, https://github.com/agentculture/substack-cli
6
+ Project-URL: Issues, https://github.com/agentculture/substack-cli/issues
7
+ Author: AgentCulture
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+
18
+ # substack-cli
19
+
20
+ Agent-first CLI to manage a Substack publication and account: publish and schedule posts, read posts and comments, run audience and post statistics, and manage subscribers. Unofficial community tool, not affiliated with Substack.
21
+
22
+ ## What you get
23
+
24
+ - **An agent-first CLI** cited from [teken](https://github.com/agentculture/teken)
25
+ (`afi-cli`) — the runtime package has no third-party dependencies.
26
+ - **A mesh identity** — `culture.yaml` (`suffix` + `backend`) and the matching
27
+ resident prompt file (`CLAUDE.md`, since this template runs
28
+ `backend: claude`). The mesh resident is one of **two separate
29
+ selections** over this clone — see
30
+ [Two selections, not one](#two-selections-not-one) below.
31
+ - **Four harness prompt files**, one per agent harness, each read by exactly
32
+ one of them (see [Prompt files by harness](#prompt-files-by-harness) below).
33
+ All four harnesses are usable interactively regardless of which one
34
+ `culture.yaml` names as the mesh resident.
35
+ - **The canonical guildmaster skill kit** (11 skills) under `.claude/skills/`,
36
+ vendored cite-don't-import. See [`docs/skill-sources.md`](docs/skill-sources.md).
37
+ - **A build + deploy baseline** — pytest, lint, the agent-first rubric gate, and
38
+ PyPI Trusted Publishing wired into GitHub Actions.
39
+
40
+ ## Prompt files by harness
41
+
42
+ Four harnesses, four root files, no shared base — each file is read by
43
+ exactly one harness:
44
+
45
+ | Harness | File(s) |
46
+ |---------|---------|
47
+ | Claude Code | [`CLAUDE.md`](CLAUDE.md) |
48
+ | Pi / associate | [`AGENTS.override.md`](AGENTS.override.md) + [`.pi/SYSTEM.md`](.pi/SYSTEM.md) |
49
+ | colleague | [`AGENTS.colleague.md`](AGENTS.colleague.md) |
50
+ | Qwen Code | [`QWEN.md`](QWEN.md) |
51
+
52
+ **Claude Code** — `CLAUDE.md` is the fullest write-up of the repo's
53
+ conventions; read it first.
54
+
55
+ **Pi / associate** — `AGENTS.override.md` replaces this directory's
56
+ `AGENTS.md`/`CLAUDE.md` in Pi's context layer, so Pi does not inherit
57
+ `CLAUDE.md`. `.pi/SYSTEM.md` replaces Pi's default system prompt with the
58
+ non-coding `associate` identity (read/find/summarize only).
59
+
60
+ **colleague** — colleague's prompt cascade is `AGENTS.md` →
61
+ `AGENTS.colleague.md` → `AGENTS.colleague.<model>.md`. This repo ships only
62
+ the middle layer: there is no `AGENTS.md` (a shared base across harnesses was
63
+ considered and rejected) and no per-model override file.
64
+
65
+ **Qwen Code** — Qwen Code reads `QWEN.md` and `AGENTS.md`; since there is no
66
+ `AGENTS.md`, `QWEN.md` is its sole source of guidance.
67
+
68
+ There is intentionally **no `AGENTS.md`** at the root — each harness gets an
69
+ unrelated file rather than cascading from a shared base.
70
+
71
+ ## Two selections, not one
72
+
73
+ It is tempting to read "switch harness" as one decision. It is actually two,
74
+ and this template exists partly to keep them separate:
75
+
76
+ 1. **The interactive harness** — which binary you run (`claude`, `pi`,
77
+ `colleague`, `qwen`). `cd` into the clone and run any of them; all four
78
+ are live simultaneously, and none of them requires editing a file or
79
+ flipping a switch. A harness can be force-selected for one invocation
80
+ (e.g. a CI smoke check) without ever touching `culture.yaml` — see
81
+ [`docs/automation-contract.md`](docs/automation-contract.md).
82
+ 2. **The mesh resident** — the single `backend` `culture.yaml` declares,
83
+ which is what the Culture daemon starts and what `steward doctor`
84
+ checks. `guild harness use <name>` changes only this.
85
+
86
+ `culture.yaml`'s `backend` affects (2) only. It never affects which harness
87
+ you can invoke interactively in (1). See
88
+ [`docs/harness-selection.md`](docs/harness-selection.md) for the full
89
+ writeup, including who reads this config and why existing siblings are not
90
+ retrofitted by this arc.
91
+
92
+ ## Quickstart
93
+
94
+ ```bash
95
+ uv sync
96
+ uv run pytest -n auto # run the test suite
97
+ uv run substack-cli whoami # identity from culture.yaml
98
+ uv run substack-cli learn # self-teaching prompt (add --json)
99
+ uv run teken cli doctor . --strict # the agent-first rubric gate CI runs
100
+ ```
101
+
102
+ ## CLI
103
+
104
+ | Verb | What it does |
105
+ |------|--------------|
106
+ | `whoami` | Report this agent's nick, version, backend, and model from `culture.yaml`. |
107
+ | `learn` | Print a structured self-teaching prompt. |
108
+ | `explain <path>` | Markdown docs for any noun/verb path. |
109
+ | `overview` | Read-only descriptive snapshot of the agent. |
110
+ | `doctor` | Check the agent-identity invariants (prompt-file-present, backend-consistency). |
111
+ | `cli overview` | Describe the CLI surface itself. |
112
+
113
+ Every command supports `--json`. Results go to stdout, errors/diagnostics to
114
+ stderr (never mixed). Exit codes: `0` success, `1` user error, `2` environment
115
+ error, `3+` reserved.
116
+
117
+ ## Make it your own
118
+
119
+ 1. Rename the package `substack_cli/` and the `substack-cli`
120
+ CLI/dist name throughout `pyproject.toml`, the package, `tests/`,
121
+ `sonar-project.properties`, and this `README.md`. The name is hard-coded in
122
+ ~100 places, so list every occurrence first — see the `git grep` discovery
123
+ command in [`CLAUDE.md`](CLAUDE.md), the authoritative rename procedure.
124
+ 2. Edit `culture.yaml` with your `suffix` and `backend`.
125
+ 3. Rewrite `CLAUDE.md` for your agent and run `/init`. Rewrite the other three
126
+ harness files (`AGENTS.override.md` + `.pi/SYSTEM.md`, `AGENTS.colleague.md`,
127
+ `QWEN.md`) too if your agent uses those harnesses — don't let them drift out
128
+ of sync with `CLAUDE.md`.
129
+ 4. Re-vendor only the skills you need from guildmaster (see
130
+ [`docs/skill-sources.md`](docs/skill-sources.md)).
131
+
132
+ See [`CLAUDE.md`](CLAUDE.md) for the full conventions (version-bump-every-PR,
133
+ the `cicd` PR lane, deploy setup).
134
+
135
+ ## License
136
+
137
+ Apache 2.0 — see [`LICENSE`](LICENSE).
@@ -0,0 +1,19 @@
1
+ substack_cli/__init__.py,sha256=mVRyVa6yeX2aqdyi7NrlPJA3V01BkJve5vkkuuWrk2c,406
2
+ substack_cli/__main__.py,sha256=bYagIxRrQAwmYxYTY6sN1weaqhJwH60XMyNQCcPgd78,182
3
+ substack_cli/cli/__init__.py,sha256=9B9ThdCOcBVXVc-deqXUG_Edcw9vm3gYA11stXLKiIs,5016
4
+ substack_cli/cli/_errors.py,sha256=VFCJak9qVwkGzusUmBn6s0HNfyhozHvjkJxU7swEPRE,1307
5
+ substack_cli/cli/_output.py,sha256=6cabSCl26xx7N0SE3-mgyd0MitvoJ4fXaUnx8SuKE24,1711
6
+ substack_cli/cli/_commands/__init__.py,sha256=DqpfVkhBo7cj6eWOpSlwx1ag0nTbrYJkV8pnPgl2gyU,70
7
+ substack_cli/cli/_commands/cli.py,sha256=frg0r-uiDzifLZG_4dhNpZSbe1bfG0-z85_9FkOhNWY,1689
8
+ substack_cli/cli/_commands/doctor.py,sha256=YUep70WZFiaXKDVBkr3gCxRlgZSs4KlgNqoxKORGWho,7864
9
+ substack_cli/cli/_commands/explain.py,sha256=RSjbTocY8EDtC4p9hX7-BDyw_CkklpwAVz84snu54qQ,1237
10
+ substack_cli/cli/_commands/learn.py,sha256=aHBK3RjMUFtEF9HNrEs_yT8QbGR-n2w9G46-OcfJJnc,3027
11
+ substack_cli/cli/_commands/overview.py,sha256=xy9fIpRbBmObcgchCngHtzdxGr22WtpLqbRELyYRlgw,3971
12
+ substack_cli/cli/_commands/whoami.py,sha256=Q8Zmh_0Gwg7umgV_DSWgqovpVAiOsHry3h3OJ0Erl3w,3658
13
+ substack_cli/explain/__init__.py,sha256=elAzIX4yqKIDUHo9VsTotcbOO_jVp9faB_ZHQZM_zes,712
14
+ substack_cli/explain/catalog.py,sha256=v6cMCeGr3tSDCwj1V1FwzMUEiiZUTd-JD28Gt68GTZU,3649
15
+ substack_cli-0.9.0.dist-info/METADATA,sha256=e7vz2eylrKNZU1eEpKOEXHHmmwqw7JvHFjjfeZwvQxo,6469
16
+ substack_cli-0.9.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
17
+ substack_cli-0.9.0.dist-info/entry_points.txt,sha256=p39AC48B8VjDZseqOUIww6BQomRyYXoAwbAzhSXNDeM,51
18
+ substack_cli-0.9.0.dist-info/licenses/LICENSE,sha256=UTsio4-AMxNDoMcES3iMDOVbdrnTy1rQD7Be7zWjqcA,11340
19
+ substack_cli-0.9.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ substack = substack_cli.cli:main