typefully-cli 0.2.1__tar.gz → 0.4.0__tar.gz
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.
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/.github/workflows/ci.yml +10 -3
- typefully_cli-0.4.0/.github/workflows/monitor.yml +72 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/.github/workflows/publish.yml +1 -1
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/CHANGELOG.md +18 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/CLAUDE.md +10 -12
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/PKG-INFO +49 -3
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/README.md +48 -2
- typefully_cli-0.4.0/collector/DEPLOYMENT.md +54 -0
- typefully_cli-0.4.0/collector/README.md +70 -0
- typefully_cli-0.4.0/collector/schema.sql +7 -0
- typefully_cli-0.4.0/collector/worker.mjs +72 -0
- typefully_cli-0.4.0/collector/worker.test.mjs +123 -0
- typefully_cli-0.4.0/collector/wrangler.example.jsonc +15 -0
- typefully_cli-0.4.0/docs/quote-parity.md +79 -0
- typefully_cli-0.4.0/docs/release-0.4.0.md +37 -0
- typefully_cli-0.4.0/docs/telemetry.md +65 -0
- typefully_cli-0.4.0/docs/upstream-parity.md +133 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/pyproject.toml +1 -1
- typefully_cli-0.4.0/scripts/monitor.py +147 -0
- typefully_cli-0.4.0/skills/typefully-cli/SKILL.md +86 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/__init__.py +1 -1
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/cli.py +308 -44
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/client.py +25 -4
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/config.py +11 -2
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/media.py +2 -1
- typefully_cli-0.4.0/src/typefully_cli/payloads.py +195 -0
- typefully_cli-0.4.0/src/typefully_cli/request-schemas.json +1220 -0
- typefully_cli-0.4.0/src/typefully_cli/telemetry.py +85 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/tests/test_cli.py +338 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/tests/test_completions.py +2 -1
- typefully_cli-0.4.0/tests/test_monitor.py +69 -0
- typefully_cli-0.4.0/tests/test_payloads.py +216 -0
- typefully_cli-0.4.0/tests/test_telemetry.py +209 -0
- typefully_cli-0.2.1/.github/workflows/monitor.yml +0 -141
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/.github/dependabot.yml +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/.gitignore +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/CONTRIBUTING.md +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/Formula/README.md +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/Formula/typefully-cli.rb +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/LICENSE +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/TODOS.md +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/docs/batch-format.md +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/requirements.lock +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/__main__.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/auth.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/batch.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/completions.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/console.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/exceptions.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/src/typefully_cli/output.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/tests/__init__.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/tests/conftest.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/tests/test_auth.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/tests/test_batch.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/tests/test_client.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/tests/test_media.py +0 -0
- {typefully_cli-0.2.1 → typefully_cli-0.4.0}/tests/test_output.py +0 -0
|
@@ -8,13 +8,13 @@ on:
|
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
11
|
+
runs-on: ubuntu-22.04 # Python 3.9 remains supported; avoid ubuntu-latest image drift.
|
|
12
12
|
strategy:
|
|
13
13
|
matrix:
|
|
14
14
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
18
18
|
|
|
19
19
|
- name: Set up Python ${{ matrix.python-version }}
|
|
20
20
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
@@ -25,7 +25,14 @@ jobs:
|
|
|
25
25
|
run: pip install -e ".[dev]"
|
|
26
26
|
|
|
27
27
|
- name: Lint
|
|
28
|
-
run: ruff check src/ tests/
|
|
28
|
+
run: ruff check --isolated --select E4,E7,E9,F --target-version py39 --line-length 100 src/ tests/ scripts/
|
|
29
29
|
|
|
30
30
|
- name: Test
|
|
31
31
|
run: python -m pytest tests/ -v
|
|
32
|
+
|
|
33
|
+
collector:
|
|
34
|
+
runs-on: ubuntu-22.04
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
|
37
|
+
- name: Test collector (mocked storage, no network)
|
|
38
|
+
run: node --test collector/worker.test.mjs
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
name: Monitor Typefully API & Competitors
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "0 8 * * 1,4" # Mon & Thu 8am UTC (1:30pm IST)
|
|
6
|
+
workflow_dispatch: {}
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: typefully-upstream-monitor
|
|
10
|
+
cancel-in-progress: false
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
issues: write
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
monitor:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
21
|
+
|
|
22
|
+
- name: Restore cache
|
|
23
|
+
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
24
|
+
with:
|
|
25
|
+
path: .monitor-cache
|
|
26
|
+
key: monitor-cache-${{ github.run_id }}
|
|
27
|
+
restore-keys: monitor-cache-
|
|
28
|
+
|
|
29
|
+
- name: Fetch and validate public upstream sources
|
|
30
|
+
run: python3 scripts/monitor.py
|
|
31
|
+
|
|
32
|
+
- name: Report changes or degraded monitoring
|
|
33
|
+
env:
|
|
34
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
35
|
+
run: |
|
|
36
|
+
python3 - <<'PYTHON'
|
|
37
|
+
import json
|
|
38
|
+
import subprocess
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
cache = Path(".monitor-cache")
|
|
42
|
+
result = json.loads((cache / "result.json").read_text())
|
|
43
|
+
if result["report_needed"]:
|
|
44
|
+
# A stable title deduplicates the same unresolved change/failure.
|
|
45
|
+
issues = json.loads(subprocess.check_output([
|
|
46
|
+
"gh", "issue", "list", "--state", "open", "--search",
|
|
47
|
+
result["fingerprint"] + " in:title",
|
|
48
|
+
"--json", "title", "--limit", "100",
|
|
49
|
+
], text=True))
|
|
50
|
+
if not any(issue["title"] == result["title"] for issue in issues):
|
|
51
|
+
subprocess.run([
|
|
52
|
+
"gh", "issue", "create", "--title", result["title"],
|
|
53
|
+
"--body-file", str(cache / "report.md"),
|
|
54
|
+
], check=True)
|
|
55
|
+
subprocess.run(["python3", "scripts/monitor.py", "--commit"], check=True)
|
|
56
|
+
PYTHON
|
|
57
|
+
|
|
58
|
+
- name: Save cache
|
|
59
|
+
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
60
|
+
with:
|
|
61
|
+
path: .monitor-cache
|
|
62
|
+
key: monitor-cache-${{ github.run_id }}
|
|
63
|
+
|
|
64
|
+
- name: Fail visibly on degraded monitoring
|
|
65
|
+
run: |
|
|
66
|
+
python3 - <<'PYTHON'
|
|
67
|
+
import json
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
result = json.loads(Path(".monitor-cache/result.json").read_text())
|
|
70
|
+
if result["failed"]:
|
|
71
|
+
raise SystemExit("Upstream fetch failed; see monitoring issue and result.json")
|
|
72
|
+
PYTHON
|
|
@@ -11,7 +11,7 @@ jobs:
|
|
|
11
11
|
id-token: write # trusted publishing
|
|
12
12
|
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
14
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
15
15
|
|
|
16
16
|
- name: Set up Python
|
|
17
17
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
- Add minimal default-on usage telemetry for interactive text commands only, after a visible disclosure. The first notice invocation sends nothing.
|
|
6
|
+
- Disable with `TYPEFULLY_TELEMETRY=0` or `typefully config set telemetry_enabled false`. CI, noninteractive, JSON and quiet runs are excluded.
|
|
7
|
+
- Send only CLI version and command category to a maintainer-owned collector; store daily aggregate counts, not unique-user IDs or content. Network infrastructure still sees source IP.
|
|
8
|
+
- Add strict collector validation, privacy documentation and mocked telemetry tests. See `docs/telemetry.md` for retention and limits.
|
|
9
|
+
|
|
10
|
+
## 0.3.0 (2026-10-29)
|
|
11
|
+
|
|
12
|
+
- Add schema-backed `draft-payload` request-file input with offline validation, per-post fields and null/false clearing; keep `--json` as output-only.
|
|
13
|
+
- Add X reply/subscriber/community flags, LinkedIn first-comment/reshare fields, upload alt text and media metadata updates.
|
|
14
|
+
- Follow social-set pagination, accept repeated tag filters and reject negative offsets; document every public API operation and unverified MCP/product-only boundaries.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
- Repair upstream monitoring: validate official public sources, expose fetch failures, deduplicate reports, and advance baselines only after successful reporting.
|
|
18
|
+
- Pin CI to Ubuntu 22.04 to retain Python 3.9 compatibility; isolate Fish completion tests from the real home directory.
|
|
19
|
+
- Extend `--quote-post-url` / `--qrt` to the documented Threads, Bluesky, Mastodon and Substack quote targets.
|
|
20
|
+
|
|
3
21
|
## 0.2.1 (2026-08-21)
|
|
4
22
|
|
|
5
23
|
- Fix trusted PyPI publishing workflow action pin.
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
Python CLI wrapping the Typefully API v2. Agent-
|
|
5
|
+
Python CLI wrapping the Typefully API v2. Agent-friendly structured JSON output with `--json`, structured errors on stderr, and configurable text output. See `skills/typefully-cli/SKILL.md` for current agent usage and `docs/upstream-parity.md` for evidence and limitations.
|
|
6
6
|
|
|
7
7
|
## Architecture
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
src/typefully_cli/
|
|
11
|
-
cli.py --
|
|
11
|
+
cli.py -- Click commands, shared options, error handler, payload builder
|
|
12
|
+
payloads.py -- Explicit JSON request input, local schema validation, media metadata
|
|
13
|
+
request-schemas.json -- Reviewed public create/update schema snapshot
|
|
12
14
|
client.py -- httpx wrapper over Typefully API v2 (rate-limited, account resolution)
|
|
13
15
|
completions.py -- Shell completions install/show (bash/zsh/fish)
|
|
14
16
|
auth.py -- API key resolution chain: flag > env > config > 1Password
|
|
@@ -29,7 +31,7 @@ src/typefully_cli/
|
|
|
29
31
|
- **Timezone conversion:** Config `timezone` (IANA name) converts naive schedule datetimes to UTC before API call. Already-tz-aware values pass through unchanged.
|
|
30
32
|
- **Partial failure:** Delete and batch loops catch per-item errors. `_handle_multi_result()` handles the 3-branch contract (all succeed / partial / all fail).
|
|
31
33
|
- **Error sanitization:** `APIError.user_message` property strips raw upstream bodies from stdout payloads. Full details stay on stderr.
|
|
32
|
-
- **API parity:**
|
|
34
|
+
- **API parity:** Map each public operation in docs/upstream-parity.md. Do not claim exhaustive MCP parity; analytics and legacy convenience fields are not verified by the current public schema.
|
|
33
35
|
|
|
34
36
|
## Dev setup
|
|
35
37
|
|
|
@@ -41,19 +43,15 @@ pip install -e ".[dev]"
|
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
45
|
python3 -m pytest tests/ -v
|
|
44
|
-
ruff check src/ tests/
|
|
46
|
+
ruff check --isolated --select E4,E7,E9,F --target-version py39 --line-length 100 src/ tests/ scripts/
|
|
45
47
|
```
|
|
46
48
|
|
|
47
49
|
## Auth for live testing
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
typefully config set default_account BungeeCEO
|
|
54
|
-
typefully config set default_platforms x,linkedin
|
|
55
|
-
typefully config set timezone Asia/Kolkata
|
|
56
|
-
```
|
|
51
|
+
Use existing authorized access from environment/config; never assume an account
|
|
52
|
+
named in documentation is a sandbox. No cloud credential is bundled with this repo.
|
|
53
|
+
Use local `--validate-only` and mocked tests first. Live test writes need explicit
|
|
54
|
+
scope and a verified social set; never publish as part of maintenance tests.
|
|
57
55
|
|
|
58
56
|
## Conventions
|
|
59
57
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: typefully-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: CLI for the Typefully API -- manage drafts, threads, and publishing from the terminal
|
|
5
5
|
Project-URL: Homepage, https://github.com/dheeraj-shah/typefully-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/dheeraj-shah/typefully-cli
|
|
@@ -37,8 +37,8 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
Publish to X, LinkedIn, Bluesky, Threads, Mastodon, Substack Notes, and X Articles from your terminal. Goes from idea to post without opening a browser.
|
|
39
39
|
|
|
40
|
-
[](https://pypi.org/project/typefully-cli/)
|
|
41
|
+
[](https://pypi.org/project/typefully-cli/)
|
|
42
42
|
[](LICENSE)
|
|
43
43
|
|
|
44
44
|
## Why
|
|
@@ -331,6 +331,14 @@ account = "MyBrand"
|
|
|
331
331
|
output_format = "text" # or "json" for agent/script use
|
|
332
332
|
```
|
|
333
333
|
|
|
334
|
+
## Usage telemetry
|
|
335
|
+
|
|
336
|
+
This unreleased branch counts CLI version and command category only through a
|
|
337
|
+
maintainer-owned collector, after a visible notice. The first notice invocation
|
|
338
|
+
sends nothing. Published 0.3.0 installations are unchanged until a new release.
|
|
339
|
+
Disable with `TYPEFULLY_TELEMETRY=0` or
|
|
340
|
+
`typefully config set telemetry_enabled false`. See [privacy and activation details](docs/telemetry.md).
|
|
341
|
+
|
|
334
342
|
## Contributing
|
|
335
343
|
|
|
336
344
|
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -338,3 +346,41 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
338
346
|
## License
|
|
339
347
|
|
|
340
348
|
MIT
|
|
349
|
+
|
|
350
|
+
## Upstream compatibility maintenance
|
|
351
|
+
|
|
352
|
+
The public-source monitor opens issues for upstream changes or failed checks; it does not
|
|
353
|
+
implement or merge updates. See the [source and capability baseline](docs/upstream-parity.md)
|
|
354
|
+
for monitored sources, known limits, and how maintenance coordinates with Dependabot.
|
|
355
|
+
|
|
356
|
+
Native quotes route to the matching selected platform (the existing `--qrt` alias still works):
|
|
357
|
+
|
|
358
|
+
```sh
|
|
359
|
+
typefully draft "My take" --platform bluesky --quote-post-url https://bsky.app/profile/example.com/post/abc
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Threads, Bluesky, Mastodon and Substack quotes apply to the first new post. Existing X
|
|
363
|
+
thread behavior is retained. See [quote compatibility notes](docs/quote-parity.md).
|
|
364
|
+
|
|
365
|
+
### Complete request input and media alt text
|
|
366
|
+
|
|
367
|
+
`draft-payload create --file draft.json --validate-only` validates the reviewed public
|
|
368
|
+
API schema offline. Remove `--validate-only` and select `--account` to submit.
|
|
369
|
+
Use `draft-payload update --draft-id ID --file update.json` for per-post settings,
|
|
370
|
+
explicit null/false clearing, or advanced fields. This is **request input**;
|
|
371
|
+
`--json` on existing commands only controls output. The new advanced commands
|
|
372
|
+
always emit JSON. Platform arrays replace content: retain posts/comment markers
|
|
373
|
+
and settings you want to keep. `publish_at: "now"` really requests publication.
|
|
374
|
+
|
|
375
|
+
`upload image.png --alt-text "Description"` sets alt text before attachment.
|
|
376
|
+
`media-update MEDIA_ID --alt-text "New description"` or `--clear-alt-text` changes
|
|
377
|
+
stored media; previously attached drafts keep their existing metadata snapshot.
|
|
378
|
+
See [the full public capability matrix and remaining MCP boundaries](docs/upstream-parity.md).
|
|
379
|
+
|
|
380
|
+
### Agent skill
|
|
381
|
+
|
|
382
|
+
The repository's [Typefully CLI skill](skills/typefully-cli/SKILL.md) describes these
|
|
383
|
+
Python CLI commands, local validation and authorization boundaries. It is distinct
|
|
384
|
+
from Typefully's official JavaScript agent skill. Check the installed CLI's help:
|
|
385
|
+
maintenance-branch features are not available in an older published package until
|
|
386
|
+
an approved release is made.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Publish to X, LinkedIn, Bluesky, Threads, Mastodon, Substack Notes, and X Articles from your terminal. Goes from idea to post without opening a browser.
|
|
4
4
|
|
|
5
|
-
[](https://pypi.org/project/typefully-cli/)
|
|
6
|
+
[](https://pypi.org/project/typefully-cli/)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
9
|
## Why
|
|
@@ -296,6 +296,14 @@ account = "MyBrand"
|
|
|
296
296
|
output_format = "text" # or "json" for agent/script use
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
+
## Usage telemetry
|
|
300
|
+
|
|
301
|
+
This unreleased branch counts CLI version and command category only through a
|
|
302
|
+
maintainer-owned collector, after a visible notice. The first notice invocation
|
|
303
|
+
sends nothing. Published 0.3.0 installations are unchanged until a new release.
|
|
304
|
+
Disable with `TYPEFULLY_TELEMETRY=0` or
|
|
305
|
+
`typefully config set telemetry_enabled false`. See [privacy and activation details](docs/telemetry.md).
|
|
306
|
+
|
|
299
307
|
## Contributing
|
|
300
308
|
|
|
301
309
|
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -303,3 +311,41 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
303
311
|
## License
|
|
304
312
|
|
|
305
313
|
MIT
|
|
314
|
+
|
|
315
|
+
## Upstream compatibility maintenance
|
|
316
|
+
|
|
317
|
+
The public-source monitor opens issues for upstream changes or failed checks; it does not
|
|
318
|
+
implement or merge updates. See the [source and capability baseline](docs/upstream-parity.md)
|
|
319
|
+
for monitored sources, known limits, and how maintenance coordinates with Dependabot.
|
|
320
|
+
|
|
321
|
+
Native quotes route to the matching selected platform (the existing `--qrt` alias still works):
|
|
322
|
+
|
|
323
|
+
```sh
|
|
324
|
+
typefully draft "My take" --platform bluesky --quote-post-url https://bsky.app/profile/example.com/post/abc
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Threads, Bluesky, Mastodon and Substack quotes apply to the first new post. Existing X
|
|
328
|
+
thread behavior is retained. See [quote compatibility notes](docs/quote-parity.md).
|
|
329
|
+
|
|
330
|
+
### Complete request input and media alt text
|
|
331
|
+
|
|
332
|
+
`draft-payload create --file draft.json --validate-only` validates the reviewed public
|
|
333
|
+
API schema offline. Remove `--validate-only` and select `--account` to submit.
|
|
334
|
+
Use `draft-payload update --draft-id ID --file update.json` for per-post settings,
|
|
335
|
+
explicit null/false clearing, or advanced fields. This is **request input**;
|
|
336
|
+
`--json` on existing commands only controls output. The new advanced commands
|
|
337
|
+
always emit JSON. Platform arrays replace content: retain posts/comment markers
|
|
338
|
+
and settings you want to keep. `publish_at: "now"` really requests publication.
|
|
339
|
+
|
|
340
|
+
`upload image.png --alt-text "Description"` sets alt text before attachment.
|
|
341
|
+
`media-update MEDIA_ID --alt-text "New description"` or `--clear-alt-text` changes
|
|
342
|
+
stored media; previously attached drafts keep their existing metadata snapshot.
|
|
343
|
+
See [the full public capability matrix and remaining MCP boundaries](docs/upstream-parity.md).
|
|
344
|
+
|
|
345
|
+
### Agent skill
|
|
346
|
+
|
|
347
|
+
The repository's [Typefully CLI skill](skills/typefully-cli/SKILL.md) describes these
|
|
348
|
+
Python CLI commands, local validation and authorization boundaries. It is distinct
|
|
349
|
+
from Typefully's official JavaScript agent skill. Check the installed CLI's help:
|
|
350
|
+
maintenance-branch features are not available in an older published package until
|
|
351
|
+
an approved release is made.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Deployment verification — 2026-10-29
|
|
2
|
+
|
|
3
|
+
Owner approved dedicated personal Cloudflare Worker + D1 creation on the confirmed
|
|
4
|
+
Workers/D1 Free plan; no plan change, credentials, or security grants were made.
|
|
5
|
+
|
|
6
|
+
- Endpoint: `https://typefully-cli-telemetry.dheeraj-personal-site.workers.dev/v1/count`
|
|
7
|
+
- Worker: `typefully-cli-telemetry`
|
|
8
|
+
- Account: `22980d1f74f2694e0642ea3cf9f384d9` (Dheeraj Shah)
|
|
9
|
+
- D1: `typefully-cli-telemetry`, ID `1c795c09-fb39-41bf-bf9d-a4f7deae6c9c`
|
|
10
|
+
- Worker version: `e251b178-c17f-44d1-99e5-ca1006ad1067`
|
|
11
|
+
- D1 read replication: disabled
|
|
12
|
+
|
|
13
|
+
Uploaded `worker.mjs` with only its default fetch export. The API upload response
|
|
14
|
+
confirmed `observability.enabled=false`, log/invocation-log/trace enabled flags
|
|
15
|
+
false, both log/trace persistence flags false, `logpush=false`, and empty tail
|
|
16
|
+
consumers. Subsequent settings readback confirmed the dedicated D1 binding,
|
|
17
|
+
`logpush=false` and empty tail consumers; disabled observability was omitted from
|
|
18
|
+
that read response. Account Logpush jobs list was empty. The workers.dev route was
|
|
19
|
+
read back enabled with preview URLs disabled.
|
|
20
|
+
|
|
21
|
+
Checks against the deployed endpoint used no real usage:
|
|
22
|
+
|
|
23
|
+
- GET `/v1/count` returned 405 with an empty body.
|
|
24
|
+
- POST of the rejected synthetic dimensions `synthetic-test` / `synthetic-test`
|
|
25
|
+
returned 400 with an empty body.
|
|
26
|
+
- D1 schema readback showed only `day`, `version`, `category`, `count`.
|
|
27
|
+
- D1 count after both checks: zero rows. No production-dimension count was sent.
|
|
28
|
+
- Local Node tests: 7 passed. SQLite schema/upsert checked locally.
|
|
29
|
+
|
|
30
|
+
This verifies the rejection path and deployed configuration, not a production
|
|
31
|
+
accepted-event end-to-end write. Accepted payload/database behavior is tested
|
|
32
|
+
locally with a mock D1 binding. Cloudflare infrastructure still receives IP and
|
|
33
|
+
transport metadata; application configuration and account Logpush checks do not
|
|
34
|
+
verify all provider-internal security retention. No claim of full anonymity.
|
|
35
|
+
|
|
36
|
+
The example Wrangler configuration remains intentionally inactive. An operator
|
|
37
|
+
must copy it and explicitly supply the above binding/approved route before future
|
|
38
|
+
deployment. Do not upgrade the Free plan automatically; quota exhaustion should
|
|
39
|
+
leave telemetry best-effort, not change CLI behavior or authorize charges.
|
|
40
|
+
|
|
41
|
+
## Prepared 0.4.0 candidate update — 12:20 UTC
|
|
42
|
+
|
|
43
|
+
The existing collector was redeployed with only the reviewed version allowlist
|
|
44
|
+
expanded from `0.3.0` to `0.3.0` and `0.4.0`; the CLI candidate is not published.
|
|
45
|
+
Downloaded deployed source confirmed both exact versions. Local mock tests accept
|
|
46
|
+
both and reject `0.4.0-rc.1`, `0.4.1`, and `0.5.0`.
|
|
47
|
+
|
|
48
|
+
Upload again confirmed all observability/logging/tracing and persistence flags
|
|
49
|
+
false, Logpush false, and no tail consumers. Settings readback confirmed the same
|
|
50
|
+
D1 binding and no Logpush/tails; account Logpush jobs remain empty. Route remains
|
|
51
|
+
enabled and previews disabled. A rejected synthetic POST returned 400 with zero
|
|
52
|
+
response bytes; a subsequent D1 count confirmed zero rows. No accepted production
|
|
53
|
+
payload was sent. No resources, paid plan
|
|
54
|
+
changes, credentials, or grants were added by this update.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Minimal usage collector
|
|
2
|
+
|
|
3
|
+
Owned Cloudflare Worker + dedicated D1 database. The owner approved the dedicated
|
|
4
|
+
Free-plan deployment on 2026-10-29. See [deployment verification](DEPLOYMENT.md) for
|
|
5
|
+
the exact recipient and checks. No credentials or deployment automation are bundled.
|
|
6
|
+
CLI publication/release remains a separate approval.
|
|
7
|
+
|
|
8
|
+
## Data contract
|
|
9
|
+
|
|
10
|
+
`POST /v1/count`, `Content-Type: application/json`, at most 256 UTF-8 bytes:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{"version":"0.3.0","category":"drafts"}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Only reviewed release versions are accepted (`VERSIONS`: `0.3.0` and the prepared
|
|
17
|
+
`0.4.0` candidate); add a version explicitly
|
|
18
|
+
when releasing. Categories: `account`, `drafts`, `publishing`, `media`, `tags`,
|
|
19
|
+
`analytics`, `comments`, `queue`. Other keys/values and URLs with query strings are
|
|
20
|
+
rejected. The application ignores all request headers except content type.
|
|
21
|
+
|
|
22
|
+
The only database columns are UTC day (generated by the server), release version,
|
|
23
|
+
category, and aggregate invocation count. An atomic upsert increments that day's
|
|
24
|
+
count; there are no per-invocation rows, identifiers, fingerprints, IP addresses,
|
|
25
|
+
user agents, content, tokens, arguments, or errors stored by the application.
|
|
26
|
+
Counts measure activity, **not unique people**, and can be inflated by anyone
|
|
27
|
+
sending valid requests: this is deliberately an unauthenticated, non-identifying
|
|
28
|
+
counter, not trustworthy billing or security data.
|
|
29
|
+
|
|
30
|
+
On each accepted request the same D1 transaction removes aggregate rows older
|
|
31
|
+
than 90 days. If traffic stops, old aggregate rows remain until another accepted
|
|
32
|
+
request or an operator deletes them. D1 backups may temporarily retain deleted
|
|
33
|
+
aggregate rows. No application request logs, traces, or Logpush are enabled in the
|
|
34
|
+
template; do not enable tail consumers or add request logging.
|
|
35
|
+
|
|
36
|
+
Cloudflare necessarily receives source IP and other transport metadata. This code
|
|
37
|
+
cannot promise that Cloudflare's independent infrastructure/security processing
|
|
38
|
+
retains none of it. Review the service's current policies and account settings
|
|
39
|
+
before enabling collection; do not describe it as fully anonymous.
|
|
40
|
+
|
|
41
|
+
## Local verification (no HTTP traffic)
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
node --test collector/worker.test.mjs
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Tests use in-memory Requests and a fake D1 binding; they never call a live endpoint.
|
|
48
|
+
|
|
49
|
+
## Deployment approval checklist
|
|
50
|
+
|
|
51
|
+
1. Approve the exact owner/account, endpoint and disclosed recipient. Verify the
|
|
52
|
+
account's current Workers/D1 billing plan, quotas and limits; do not assume zero
|
|
53
|
+
cost from the word "free". Do not upgrade a plan or grant new credentials.
|
|
54
|
+
2. Approve creation of a dedicated Worker and dedicated D1 database. Do not reuse a
|
|
55
|
+
newsletter/customer database or Worker with request logging enabled.
|
|
56
|
+
3. Copy `wrangler.example.jsonc` to a private deployment configuration, add the
|
|
57
|
+
approved D1 binding named `COUNTS`, and apply `schema.sql` to that database.
|
|
58
|
+
4. Verify observability logs/traces, invocation logs, Logpush and tail consumers are
|
|
59
|
+
disabled in effective deployed settings, including any account-level exports.
|
|
60
|
+
Review platform retention separately. Enable only the approved HTTPS route.
|
|
61
|
+
5. Update the CLI compiled endpoint and its disclosure together after approval.
|
|
62
|
+
Run mocked tests and review the diff. Publishing/releasing remains a separate
|
|
63
|
+
approval; deployment is not permission to release the CLI.
|
|
64
|
+
6. Do not perform a real telemetry smoke test without approval for that payload and
|
|
65
|
+
recipient. A health check must not record an event.
|
|
66
|
+
|
|
67
|
+
Current API references used when preparing this proposal:
|
|
68
|
+
[Workers best practices](https://developers.cloudflare.com/workers/best-practices/workers-best-practices/),
|
|
69
|
+
[D1 prepared statements](https://developers.cloudflare.com/d1/worker-api/prepared-statements/),
|
|
70
|
+
[Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// This collector deliberately never records request headers, IPs, or event-level data.
|
|
2
|
+
const VERSIONS = new Set(['0.3.0', '0.4.0']);
|
|
3
|
+
const CATEGORIES = new Set(['account', 'drafts', 'publishing', 'media', 'tags', 'analytics', 'comments', 'queue']);
|
|
4
|
+
const MAX_BYTES = 256;
|
|
5
|
+
|
|
6
|
+
async function readPayload(request) {
|
|
7
|
+
if (!request.body) throw new Error('invalid');
|
|
8
|
+
const reader = request.body.getReader();
|
|
9
|
+
const chunks = [];
|
|
10
|
+
let size = 0;
|
|
11
|
+
try {
|
|
12
|
+
while (true) {
|
|
13
|
+
const { done, value } = await reader.read();
|
|
14
|
+
if (done) break;
|
|
15
|
+
size += value.byteLength;
|
|
16
|
+
if (size > MAX_BYTES) {
|
|
17
|
+
await reader.cancel();
|
|
18
|
+
throw new Error('invalid');
|
|
19
|
+
}
|
|
20
|
+
chunks.push(value);
|
|
21
|
+
}
|
|
22
|
+
} finally {
|
|
23
|
+
reader.releaseLock();
|
|
24
|
+
}
|
|
25
|
+
const bytes = new Uint8Array(size);
|
|
26
|
+
let offset = 0;
|
|
27
|
+
for (const chunk of chunks) {
|
|
28
|
+
bytes.set(chunk, offset);
|
|
29
|
+
offset += chunk.byteLength;
|
|
30
|
+
}
|
|
31
|
+
const data = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
32
|
+
if (!data || Array.isArray(data) || typeof data !== 'object'
|
|
33
|
+
|| Object.keys(data).sort().join(',') !== 'category,version'
|
|
34
|
+
|| !VERSIONS.has(data.version) || !CATEGORIES.has(data.category)) {
|
|
35
|
+
throw new Error('invalid');
|
|
36
|
+
}
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const response = (status) => new Response(null, {
|
|
41
|
+
status, headers: { 'Cache-Control': 'no-store' },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export default {
|
|
45
|
+
async fetch(request, env) {
|
|
46
|
+
const url = new URL(request.url);
|
|
47
|
+
if (url.pathname !== '/v1/count' || url.search) return response(404);
|
|
48
|
+
if (request.method !== 'POST') return response(405);
|
|
49
|
+
if (request.headers.get('content-type') !== 'application/json') return response(415);
|
|
50
|
+
let data;
|
|
51
|
+
try {
|
|
52
|
+
data = await readPayload(request);
|
|
53
|
+
} catch {
|
|
54
|
+
return response(400);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
// Only UTC day, approved release version/category, and an aggregate count reach D1.
|
|
58
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
59
|
+
const cutoff = new Date(Date.now() - 90 * 86400000).toISOString().slice(0, 10);
|
|
60
|
+
await env.COUNTS.batch([
|
|
61
|
+
env.COUNTS.prepare('DELETE FROM daily_counts WHERE day < ?').bind(cutoff),
|
|
62
|
+
env.COUNTS.prepare(`INSERT INTO daily_counts (day, version, category, count)
|
|
63
|
+
VALUES (?, ?, ?, 1)
|
|
64
|
+
ON CONFLICT(day, version, category) DO UPDATE SET count = count + 1`)
|
|
65
|
+
.bind(day, data.version, data.category),
|
|
66
|
+
]);
|
|
67
|
+
return response(204);
|
|
68
|
+
} catch {
|
|
69
|
+
return response(503);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import worker from './worker.mjs';
|
|
4
|
+
const CATEGORIES = ['account', 'drafts', 'publishing', 'media', 'tags', 'analytics', 'comments', 'queue'];
|
|
5
|
+
|
|
6
|
+
function database(fail = false) {
|
|
7
|
+
const batches = [];
|
|
8
|
+
return {
|
|
9
|
+
batches,
|
|
10
|
+
prepare(sql) { return { bind: (...values) => ({ sql, values }) }; },
|
|
11
|
+
async batch(statements) {
|
|
12
|
+
if (fail) throw new Error('private backend details');
|
|
13
|
+
batches.push(statements);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function request(body = { version: '0.3.0', category: 'drafts' }, options = {}) {
|
|
18
|
+
return new Request('https://collector.invalid/v1/count', {
|
|
19
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: typeof body === 'string' ? body : JSON.stringify(body), ...options,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('every category stores only daily aggregate dimensions; never headers', async () => {
|
|
25
|
+
for (const category of CATEGORIES) {
|
|
26
|
+
const db = database();
|
|
27
|
+
const req = request({ version: '0.3.0', category });
|
|
28
|
+
req.headers.set('CF-Connecting-IP', '192.0.2.1');
|
|
29
|
+
req.headers.set('User-Agent', 'private-machine-name');
|
|
30
|
+
req.headers.set('Authorization', 'Bearer secret');
|
|
31
|
+
const res = await worker.fetch(req, { COUNTS: db });
|
|
32
|
+
assert.equal(res.status, 204);
|
|
33
|
+
assert.equal(res.headers.get('Cache-Control'), 'no-store');
|
|
34
|
+
assert.equal(db.batches.length, 1);
|
|
35
|
+
const [cleanup, increment] = db.batches[0];
|
|
36
|
+
assert.match(cleanup.sql, /DELETE FROM daily_counts/);
|
|
37
|
+
assert.match(cleanup.values[0], /^\d{4}-\d{2}-\d{2}$/);
|
|
38
|
+
assert.match(increment.sql, /ON CONFLICT.*DO UPDATE SET count = count \+ 1/);
|
|
39
|
+
assert.deepEqual(increment.values, [new Date().toISOString().slice(0, 10), '0.3.0', category]);
|
|
40
|
+
assert.doesNotMatch(JSON.stringify(db.batches), /secret|private-machine-name|192\.0/);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('accepts both reviewed release versions without broadening the allowlist', async () => {
|
|
45
|
+
for (const version of ['0.3.0', '0.4.0']) {
|
|
46
|
+
const db = database();
|
|
47
|
+
assert.equal((await worker.fetch(request({ version, category: 'drafts' }), { COUNTS: db })).status, 204);
|
|
48
|
+
assert.equal(db.batches[0][1].values[1], version);
|
|
49
|
+
}
|
|
50
|
+
for (const version of ['0.4.0-rc.1', '0.4.1', '0.5.0']) {
|
|
51
|
+
const db = database();
|
|
52
|
+
assert.equal((await worker.fetch(request({ version, category: 'drafts' }), { COUNTS: db })).status, 400);
|
|
53
|
+
assert.deepEqual(db.batches, []);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('rejects malformed, unapproved, and extra fields before storage', async () => {
|
|
58
|
+
for (const body of [
|
|
59
|
+
'not json', 'null', '[]', '"text"', '{}',
|
|
60
|
+
{ version: '0.3.0', category: 'drafts', content: 'secret' },
|
|
61
|
+
{ version: '0.3.0', category: 'unknown' },
|
|
62
|
+
{ version: 'my-private-build', category: 'drafts' },
|
|
63
|
+
{ version: '99.99.99', category: 'drafts' },
|
|
64
|
+
{ version: 3, category: 'drafts' },
|
|
65
|
+
{ version: '0.3.0', category: ['drafts'] },
|
|
66
|
+
' '.repeat(257),
|
|
67
|
+
]) {
|
|
68
|
+
const db = database();
|
|
69
|
+
const res = await worker.fetch(request(body), { COUNTS: db });
|
|
70
|
+
assert.equal(res.status, 400, JSON.stringify(body));
|
|
71
|
+
assert.deepEqual(db.batches, []);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('bounded streamed body ignores forged Content-Length', async () => {
|
|
76
|
+
const stream = new ReadableStream({
|
|
77
|
+
start(controller) {
|
|
78
|
+
controller.enqueue(new TextEncoder().encode(' '.repeat(200)));
|
|
79
|
+
controller.enqueue(new TextEncoder().encode(' '.repeat(200)));
|
|
80
|
+
controller.close();
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const db = database();
|
|
84
|
+
const req = request('', { body: stream, duplex: 'half', headers: {
|
|
85
|
+
'Content-Type': 'application/json', 'Content-Length': '1',
|
|
86
|
+
} });
|
|
87
|
+
assert.equal((await worker.fetch(req, { COUNTS: db })).status, 400);
|
|
88
|
+
assert.deepEqual(db.batches, []);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('rejects invalid UTF-8, route, query, method and content type', async () => {
|
|
92
|
+
const db = database();
|
|
93
|
+
for (const [req, status] of [
|
|
94
|
+
[request('', { body: new Uint8Array([0xff]) }), 400],
|
|
95
|
+
[new Request('https://collector.invalid/'), 404],
|
|
96
|
+
[new Request('https://collector.invalid/v1/count?secret=1'), 404],
|
|
97
|
+
[new Request('https://collector.invalid/v1/count'), 405],
|
|
98
|
+
[request('{}', { headers: { 'Content-Type': 'text/plain' } }), 415],
|
|
99
|
+
]) assert.equal((await worker.fetch(req, { COUNTS: db })).status, status);
|
|
100
|
+
assert.deepEqual(db.batches, []);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('storage errors do not expose backend details or fail open', async () => {
|
|
104
|
+
const res = await worker.fetch(request(), { COUNTS: database(true) });
|
|
105
|
+
assert.equal(res.status, 503);
|
|
106
|
+
assert.equal(await res.text(), '');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('deployment template keeps public routes and application logging disabled', async () => {
|
|
110
|
+
const { readFile } = await import('node:fs/promises');
|
|
111
|
+
const template = await readFile(new URL('./wrangler.example.jsonc', import.meta.url), 'utf8');
|
|
112
|
+
const cfg = JSON.parse(template.replace(/\/\/.*$/gm, ''));
|
|
113
|
+
assert.equal(cfg.workers_dev, false);
|
|
114
|
+
assert.equal(cfg.preview_urls, false);
|
|
115
|
+
assert.equal(cfg.logpush, false);
|
|
116
|
+
assert.equal(cfg.observability.enabled, false);
|
|
117
|
+
assert.equal(cfg.observability.logs.enabled, false);
|
|
118
|
+
assert.equal(cfg.observability.logs.invocation_logs, false);
|
|
119
|
+
assert.equal(cfg.observability.traces.enabled, false);
|
|
120
|
+
assert.equal(cfg.routes, undefined);
|
|
121
|
+
assert.equal(cfg.account_id, undefined);
|
|
122
|
+
assert.equal(cfg.d1_databases, undefined);
|
|
123
|
+
});
|