promptmend 0.19.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.
- promptmend-0.19.0/.env.example +118 -0
- promptmend-0.19.0/.gitattributes +3 -0
- promptmend-0.19.0/.github/ISSUE_TEMPLATE/bug_report.yml +51 -0
- promptmend-0.19.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- promptmend-0.19.0/.github/ISSUE_TEMPLATE/feature_request.yml +21 -0
- promptmend-0.19.0/.github/PULL_REQUEST_TEMPLATE.md +17 -0
- promptmend-0.19.0/.github/copilot-instructions.md +23 -0
- promptmend-0.19.0/.github/dependabot.yml +32 -0
- promptmend-0.19.0/.github/workflows/release.yml +287 -0
- promptmend-0.19.0/.github/workflows/secret-scan.yml +25 -0
- promptmend-0.19.0/.github/workflows/test.yml +93 -0
- promptmend-0.19.0/.gitignore +19 -0
- promptmend-0.19.0/.gitleaksignore +4 -0
- promptmend-0.19.0/.pre-commit-config.yaml +36 -0
- promptmend-0.19.0/CHANGELOG.md +1062 -0
- promptmend-0.19.0/CLAUDE.md +501 -0
- promptmend-0.19.0/CONTRIBUTING.md +518 -0
- promptmend-0.19.0/LICENSE +21 -0
- promptmend-0.19.0/PKG-INFO +1099 -0
- promptmend-0.19.0/README.md +1083 -0
- promptmend-0.19.0/SECURITY.md +38 -0
- promptmend-0.19.0/docs/benchmark.md +215 -0
- promptmend-0.19.0/docs/interface.svg +210 -0
- promptmend-0.19.0/docs/prompt-candidates/PLAN.md +237 -0
- promptmend-0.19.0/espanso/match/prompts-core.yml +8 -0
- promptmend-0.19.0/espanso/match/prompts-llm.yml +136 -0
- promptmend-0.19.0/espanso/match/prompts-template.yml +54 -0
- promptmend-0.19.0/pyproject.toml +86 -0
- promptmend-0.19.0/scripts/bench_models.py +1785 -0
- promptmend-0.19.0/scripts/brew_formula.py +370 -0
- promptmend-0.19.0/scripts/check_tool_lock.py +87 -0
- promptmend-0.19.0/scripts/check_wheel.py +180 -0
- promptmend-0.19.0/scripts/install_macos.sh +63 -0
- promptmend-0.19.0/scripts/install_windows.ps1 +77 -0
- promptmend-0.19.0/scripts/release_notes.py +108 -0
- promptmend-0.19.0/scripts/update_match_history.py +145 -0
- promptmend-0.19.0/src/promptmend/__init__.py +7 -0
- promptmend-0.19.0/src/promptmend/assets.py +83 -0
- promptmend-0.19.0/src/promptmend/cli.py +688 -0
- promptmend-0.19.0/src/promptmend/clipboard_guard.py +163 -0
- promptmend-0.19.0/src/promptmend/commands/__init__.py +6 -0
- promptmend-0.19.0/src/promptmend/commands/common.py +246 -0
- promptmend-0.19.0/src/promptmend/commands/doctor.py +63 -0
- promptmend-0.19.0/src/promptmend/commands/profiles.py +91 -0
- promptmend-0.19.0/src/promptmend/commands/settings.py +424 -0
- promptmend-0.19.0/src/promptmend/commands/setup.py +462 -0
- promptmend-0.19.0/src/promptmend/commands/ui.py +46 -0
- promptmend-0.19.0/src/promptmend/commands/usage.py +219 -0
- promptmend-0.19.0/src/promptmend/config.py +883 -0
- promptmend-0.19.0/src/promptmend/config_files.py +469 -0
- promptmend-0.19.0/src/promptmend/config_store.py +920 -0
- promptmend-0.19.0/src/promptmend/deploy.py +776 -0
- promptmend-0.19.0/src/promptmend/doctor.py +699 -0
- promptmend-0.19.0/src/promptmend/factory.py +261 -0
- promptmend-0.19.0/src/promptmend/gate.py +82 -0
- promptmend-0.19.0/src/promptmend/history.py +1050 -0
- promptmend-0.19.0/src/promptmend/match_history.py +51 -0
- promptmend-0.19.0/src/promptmend/previous_install.py +377 -0
- promptmend-0.19.0/src/promptmend/profiles.py +168 -0
- promptmend-0.19.0/src/promptmend/prompt_builder.py +216 -0
- promptmend-0.19.0/src/promptmend/prompts/default.md +221 -0
- promptmend-0.19.0/src/promptmend/prompts/general.md +13 -0
- promptmend-0.19.0/src/promptmend/providers/anthropic.py +76 -0
- promptmend-0.19.0/src/promptmend/providers/base.py +398 -0
- promptmend-0.19.0/src/promptmend/providers/ollama.py +59 -0
- promptmend-0.19.0/src/promptmend/providers/openai_compatible.py +84 -0
- promptmend-0.19.0/src/promptmend/providers/usage.py +243 -0
- promptmend-0.19.0/src/promptmend/recorder.py +120 -0
- promptmend-0.19.0/src/promptmend/redaction.py +473 -0
- promptmend-0.19.0/src/promptmend/relocate.py +144 -0
- promptmend-0.19.0/src/promptmend/smoke.py +132 -0
- promptmend-0.19.0/src/promptmend/tui/__init__.py +7 -0
- promptmend-0.19.0/src/promptmend/tui/app.py +223 -0
- promptmend-0.19.0/src/promptmend/tui/brand.py +69 -0
- promptmend-0.19.0/src/promptmend/tui/home.py +187 -0
- promptmend-0.19.0/src/promptmend/tui/intro.py +90 -0
- promptmend-0.19.0/src/promptmend/tui/modals.py +163 -0
- promptmend-0.19.0/src/promptmend/tui/panes.py +1120 -0
- promptmend-0.19.0/src/promptmend/tui/previous.py +398 -0
- promptmend-0.19.0/src/promptmend/tui/state.py +79 -0
- promptmend-0.19.0/src/promptmend/tui/teach.py +113 -0
- promptmend-0.19.0/tests/bench_module.py +53 -0
- promptmend-0.19.0/tests/conftest.py +258 -0
- promptmend-0.19.0/tests/golden/no-args-help.txt +17 -0
- promptmend-0.19.0/tests/snapshots/about.svg +216 -0
- promptmend-0.19.0/tests/snapshots/diagnostics.svg +211 -0
- promptmend-0.19.0/tests/snapshots/history.svg +213 -0
- promptmend-0.19.0/tests/snapshots/home-70.svg +163 -0
- promptmend-0.19.0/tests/snapshots/home-80.svg +163 -0
- promptmend-0.19.0/tests/snapshots/home-high-contrast.svg +210 -0
- promptmend-0.19.0/tests/snapshots/home.svg +210 -0
- promptmend-0.19.0/tests/snapshots/intro-narrow.svg +150 -0
- promptmend-0.19.0/tests/snapshots/intro.svg +213 -0
- promptmend-0.19.0/tests/snapshots/previous-install.svg +207 -0
- promptmend-0.19.0/tests/snapshots/profiles.svg +207 -0
- promptmend-0.19.0/tests/snapshots/providers.svg +208 -0
- promptmend-0.19.0/tests/snapshots/triggers.svg +208 -0
- promptmend-0.19.0/tests/test_bench.py +1330 -0
- promptmend-0.19.0/tests/test_brew_formula.py +299 -0
- promptmend-0.19.0/tests/test_check_tool_lock.py +104 -0
- promptmend-0.19.0/tests/test_cli.py +1178 -0
- promptmend-0.19.0/tests/test_clipboard_guard.py +295 -0
- promptmend-0.19.0/tests/test_commands.py +1573 -0
- promptmend-0.19.0/tests/test_config.py +896 -0
- promptmend-0.19.0/tests/test_config_store.py +1052 -0
- promptmend-0.19.0/tests/test_deploy.py +1519 -0
- promptmend-0.19.0/tests/test_docs.py +228 -0
- promptmend-0.19.0/tests/test_doctor.py +451 -0
- promptmend-0.19.0/tests/test_factory.py +655 -0
- promptmend-0.19.0/tests/test_gate.py +189 -0
- promptmend-0.19.0/tests/test_history.py +1056 -0
- promptmend-0.19.0/tests/test_install_scripts.py +35 -0
- promptmend-0.19.0/tests/test_live.py +100 -0
- promptmend-0.19.0/tests/test_previous_install.py +1247 -0
- promptmend-0.19.0/tests/test_prompts.py +228 -0
- promptmend-0.19.0/tests/test_providers.py +831 -0
- promptmend-0.19.0/tests/test_recorder.py +445 -0
- promptmend-0.19.0/tests/test_redaction.py +812 -0
- promptmend-0.19.0/tests/test_release.py +196 -0
- promptmend-0.19.0/tests/test_relocate.py +535 -0
- promptmend-0.19.0/tests/test_strip_thinking.py +59 -0
- promptmend-0.19.0/tests/test_trigger_contract.py +408 -0
- promptmend-0.19.0/tests/test_triggers.py +278 -0
- promptmend-0.19.0/tests/test_tui.py +2039 -0
- promptmend-0.19.0/tests/test_tui_home.py +258 -0
- promptmend-0.19.0/tests/test_tui_snapshots.py +281 -0
- promptmend-0.19.0/tests/test_tui_teach.py +80 -0
- promptmend-0.19.0/tests/test_usage.py +514 -0
- promptmend-0.19.0/tests/test_user_profiles.py +460 -0
- promptmend-0.19.0/tests/test_version.py +11 -0
- promptmend-0.19.0/tests/test_yaml.py +291 -0
- promptmend-0.19.0/uv.lock +766 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Copy to .env and fill in. The CLI loads this file without overriding real env.
|
|
2
|
+
# Never commit the real .env. It is gitignored. Keep it outside the repository: put it in
|
|
3
|
+
# ~/.config/promptmend/.env ($XDG_CONFIG_HOME/promptmend/.env when XDG_CONFIG_HOME is
|
|
4
|
+
# set; %APPDATA%\promptmend\.env on Windows), or set
|
|
5
|
+
# PROMPTMEND_ENV=/path/to/.env in the real environment (not in a .env file). The CLI also
|
|
6
|
+
# reads the .env in this repository (editable install), but an API key there sits in the
|
|
7
|
+
# working tree. Once config.toml exists in the config folder above, no .env is read (unless
|
|
8
|
+
# PROMPTMEND_ENV names one). Save it as UTF-8. An unquoted value ends at " #" (a
|
|
9
|
+
# comment): quote a value that contains " #", such as a regex or a persona.
|
|
10
|
+
#
|
|
11
|
+
# Only the key and the persona are set below. Every other line shows its default, commented
|
|
12
|
+
# out: uncomment a line only to change it, so later default changes still reach you.
|
|
13
|
+
|
|
14
|
+
# PROMPT_PROVIDER=openrouter
|
|
15
|
+
# PROMPT_PROFILE=default
|
|
16
|
+
# Your role, stated in the first person. Opens the CONTEXT section of the `default`
|
|
17
|
+
# profile's rewrite and the `-p-` snippet. Leave empty for no fixed persona.
|
|
18
|
+
PROMPT_PERSONA=
|
|
19
|
+
# PROMPT_PERSONA="I am working as a Head of Data at Example Corp."
|
|
20
|
+
# paste (the rewrite replaces the trigger) or clipboard (the trigger vanishes and the rewrite
|
|
21
|
+
# is on the clipboard; error markers still paste). improve --output overrides it per call.
|
|
22
|
+
# PROMPT_OUTPUT=paste
|
|
23
|
+
# PROMPT_TIMEOUT_SECONDS=30
|
|
24
|
+
# Timeout for the pro tier (-ip-), which runs a slower reasoning model.
|
|
25
|
+
# PROMPT_PRO_TIMEOUT_SECONDS=60
|
|
26
|
+
# Profile for the pro tier (-ip-, and -if- when it runs OPENROUTER_PRO_MODEL); empty uses
|
|
27
|
+
# PROMPT_PROFILE, so both tiers send the same prompt.
|
|
28
|
+
# PROMPT_PRO_PROFILE=
|
|
29
|
+
# Your own profiles are <name>.md files in ~/.config/promptmend/profiles/
|
|
30
|
+
# (%APPDATA%\promptmend\profiles\ on Windows). One named like a built-in (default,
|
|
31
|
+
# general) is ignored unless that built-in is listed here, comma-separated.
|
|
32
|
+
# PROMPT_PROFILE_OVERRIDES=
|
|
33
|
+
# Empty (PROMPT_TEMPERATURE=) sends no temperature, for models that reject the field;
|
|
34
|
+
# leaving it unset keeps 0.2.
|
|
35
|
+
# PROMPT_TEMPERATURE=0.2
|
|
36
|
+
|
|
37
|
+
# Local providers are ungated only on localhost. Pointed at another machine, or with an
|
|
38
|
+
# Ollama cloud model (a :cloud or -cloud tag, any case), the draft leaves this one, so the
|
|
39
|
+
# data-protection gate applies as it does to the cloud providers. Unlike the cloud providers
|
|
40
|
+
# they accept http:// for another machine, which sends the draft in clear text over the
|
|
41
|
+
# network: use https:// (or an SSH tunnel to localhost) there.
|
|
42
|
+
|
|
43
|
+
# Ollama
|
|
44
|
+
# OLLAMA_BASE_URL=http://localhost:11434
|
|
45
|
+
# OLLAMA_MODEL=qwen3:8b
|
|
46
|
+
# OLLAMA_THINK=false
|
|
47
|
+
|
|
48
|
+
# LM Studio (OpenAI-compatible, local, no key needed)
|
|
49
|
+
# LMSTUDIO_BASE_URL=http://localhost:1234/v1
|
|
50
|
+
# LMSTUDIO_MODEL=local-model
|
|
51
|
+
|
|
52
|
+
# OpenRouter (cloud, default provider). Leave key empty if company policy
|
|
53
|
+
# does not permit sending drafts off-machine; the data-protection gate blocks
|
|
54
|
+
# sensitive content regardless. For local-only use set PROMPT_LOCAL_ONLY=true below:
|
|
55
|
+
# the cloud triggers name their provider, so PROMPT_PROVIDER does not change them.
|
|
56
|
+
OPENROUTER_API_KEY=
|
|
57
|
+
# OPENROUTER_MODEL=google/gemini-3.5-flash-lite
|
|
58
|
+
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
|
59
|
+
# OPENROUTER_MAX_TOKENS=2400
|
|
60
|
+
# Pin one serving endpoint. OpenRouter routes a model across many hosts that differ in
|
|
61
|
+
# latency, price and output quality; see the benchmark section in README.md. List the tags
|
|
62
|
+
# with GET https://openrouter.ai/api/v1/models/<author>/<slug>/endpoints. Leave empty for
|
|
63
|
+
# OpenRouter's own blended routing.
|
|
64
|
+
# OPENROUTER_PROVIDER=google-ai-studio/flex
|
|
65
|
+
# true keeps the pin a preference, so a dead endpoint falls back instead of erroring.
|
|
66
|
+
# false pins hard: predictable cost and latency, but outages become visible failures.
|
|
67
|
+
# OPENROUTER_ALLOW_FALLBACKS=true
|
|
68
|
+
# Data policy for every OpenRouter call (both tiers): deny routes only to endpoints that
|
|
69
|
+
# do not store or train on requests (fewer endpoints; a pin that does not qualify falls
|
|
70
|
+
# back, or fails with ALLOW_FALLBACKS=false), allow permits them. Empty sends nothing, so
|
|
71
|
+
# your OpenRouter account setting applies.
|
|
72
|
+
# OPENROUTER_DATA_COLLECTION=
|
|
73
|
+
# Reasoning effort (none/minimal/low/medium/high). Leave empty for models without a
|
|
74
|
+
# reasoning control. Gemini 3.x cannot turn thinking off; minimal keeps it fast.
|
|
75
|
+
# OPENROUTER_REASONING_EFFORT=minimal
|
|
76
|
+
# Pro tier (-ip- / --tier pro): a reasoning model for hard, multi-part drafts.
|
|
77
|
+
# OPENROUTER_PRO_MODEL=openai/gpt-6-luna
|
|
78
|
+
# OPENROUTER_PRO_PROVIDER=openai
|
|
79
|
+
# OPENROUTER_PRO_REASONING_EFFORT=low
|
|
80
|
+
# Output cap for the pro tier only (its reasoning counts against it). Empty uses
|
|
81
|
+
# OPENROUTER_MAX_TOKENS for both tiers.
|
|
82
|
+
# OPENROUTER_PRO_MAX_TOKENS=
|
|
83
|
+
|
|
84
|
+
# Anthropic / Claude (cloud). Leave key empty unless policy permits cloud use.
|
|
85
|
+
# ANTHROPIC_API_KEY=
|
|
86
|
+
# ANTHROPIC_MODEL=claude-sonnet-5
|
|
87
|
+
# ANTHROPIC_BASE_URL=https://api.anthropic.com
|
|
88
|
+
# ANTHROPIC_MAX_TOKENS=2400
|
|
89
|
+
|
|
90
|
+
# Set true only to allow cloud calls on every flagged draft, permanently. To send one draft
|
|
91
|
+
# whose findings are only labels, IDs, emails or IBANs, use the -iok- trigger instead.
|
|
92
|
+
# ALLOW_CLOUD_OVERRIDE=false
|
|
93
|
+
# Set true to refuse every provider that can send the draft off this machine (-i-, -ip-, -if-,
|
|
94
|
+
# -iok-, -ic-, and local providers pointed elsewhere); -il- and -ilm- on localhost keep working.
|
|
95
|
+
# PROMPT_LOCAL_ONLY=false
|
|
96
|
+
# Set true when your localhost Ollama / LM Studio is a relay to a cloud API (LiteLLM, an SSH
|
|
97
|
+
# tunnel), or when OLLAMA_MODEL is a local alias of a cloud model (made with ollama cp, so its
|
|
98
|
+
# name has no cloud tag and counts as local): -il- and -ilm- then
|
|
99
|
+
# run the gate too. PROMPT_LOCAL_ONLY still allows them.
|
|
100
|
+
# PROMPT_GATE_LOCAL=false
|
|
101
|
+
# Your own ;-separated, case-insensitive regexes the gate also blocks on, e.g. project code
|
|
102
|
+
# names or customer-ID formats. Write a literal ; as \x3b. An invalid regex is rejected when
|
|
103
|
+
# settings load. Each pattern runs on every draft and on every value written to the usage
|
|
104
|
+
# history: avoid nested quantifiers such as (\w+\s?)+, which can take exponential time and
|
|
105
|
+
# stall the trigger; use a literal, a fixed count (\d{6}) or a bounded repeat (\w{1,20}).
|
|
106
|
+
# PROMPT_EXTRA_PATTERNS=
|
|
107
|
+
# PROMPT_EXTRA_PATTERNS="project[- ]falcon;CUST-\d{6}"
|
|
108
|
+
|
|
109
|
+
# Local usage history: metadata only (trigger, model, tokens, reported cost, latency), never
|
|
110
|
+
# your draft, rewrite, persona or keys; kept on this device in ~/.local/share/promptmend
|
|
111
|
+
# (%LOCALAPPDATA%\promptmend on Windows). false keeps no history.
|
|
112
|
+
# PROMPT_HISTORY=true
|
|
113
|
+
# Days a history record is kept (1 to 36500); each recorded run prunes older ones.
|
|
114
|
+
# PROMPT_HISTORY_RETENTION_DAYS=365
|
|
115
|
+
|
|
116
|
+
# The full-screen interface (`promptmend ui`) opens with a short intro; false skips it
|
|
117
|
+
# (as `promptmend ui --no-intro` does). The triggers never read this.
|
|
118
|
+
# PROMPT_UI_INTRO=true
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something does not work as documented
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Please do not paste API keys, customer data or confidential drafts. Replace them with fake
|
|
9
|
+
values. Security problems belong in a private report (see SECURITY.md).
|
|
10
|
+
- type: textarea
|
|
11
|
+
id: what-happened
|
|
12
|
+
attributes:
|
|
13
|
+
label: What happened
|
|
14
|
+
description: "Include the exact `[promptmend: …]` message if you got one."
|
|
15
|
+
validations:
|
|
16
|
+
required: true
|
|
17
|
+
- type: textarea
|
|
18
|
+
id: expected
|
|
19
|
+
attributes:
|
|
20
|
+
label: What you expected
|
|
21
|
+
validations:
|
|
22
|
+
required: true
|
|
23
|
+
- type: textarea
|
|
24
|
+
id: reproduce
|
|
25
|
+
attributes:
|
|
26
|
+
label: Steps to reproduce
|
|
27
|
+
description: Trigger or CLI command, provider, profile, and a fake draft that shows the problem.
|
|
28
|
+
placeholder: |
|
|
29
|
+
1. echo "…" | promptmend improve --provider openrouter --source stdin
|
|
30
|
+
2. …
|
|
31
|
+
validations:
|
|
32
|
+
required: true
|
|
33
|
+
- type: dropdown
|
|
34
|
+
id: os
|
|
35
|
+
attributes:
|
|
36
|
+
label: Operating system
|
|
37
|
+
options: [macOS, Windows, Linux]
|
|
38
|
+
validations:
|
|
39
|
+
required: true
|
|
40
|
+
- type: dropdown
|
|
41
|
+
id: provider
|
|
42
|
+
attributes:
|
|
43
|
+
label: Provider
|
|
44
|
+
options: [openrouter, anthropic, ollama, lmstudio, not provider related]
|
|
45
|
+
validations:
|
|
46
|
+
required: true
|
|
47
|
+
- type: input
|
|
48
|
+
id: versions
|
|
49
|
+
attributes:
|
|
50
|
+
label: Versions
|
|
51
|
+
description: "Output of `uv tool list | grep promptmend` and `espanso --version`, plus the model if relevant."
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Suggest a trigger, profile, provider or improvement
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: Problem
|
|
9
|
+
description: What are you trying to do, and what gets in the way today?
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: proposal
|
|
14
|
+
attributes:
|
|
15
|
+
label: Proposed solution
|
|
16
|
+
validations:
|
|
17
|
+
required: true
|
|
18
|
+
- type: textarea
|
|
19
|
+
id: alternatives
|
|
20
|
+
attributes:
|
|
21
|
+
label: Alternatives considered
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## What and why
|
|
2
|
+
|
|
3
|
+
<!-- What does this change, and what problem does it solve? Link the issue if there is one. -->
|
|
4
|
+
|
|
5
|
+
## How it was tested
|
|
6
|
+
|
|
7
|
+
<!-- Commands you ran. For prompt or provider changes, include `pytest -m live` and benchmark
|
|
8
|
+
before/after pass rates. -->
|
|
9
|
+
|
|
10
|
+
## Checklist
|
|
11
|
+
|
|
12
|
+
- [ ] `uv run pytest`, `uv run ruff check .`, `uv run ruff format --check .` and `uv run mypy` pass
|
|
13
|
+
- [ ] Tests added or updated for the behaviour change
|
|
14
|
+
- [ ] Cloud providers are built only through `factory.make_provider()` (gate intact)
|
|
15
|
+
- [ ] CLI output keeps the Espanso contract: no trailing newline, errors as `[promptmend: …]`
|
|
16
|
+
- [ ] CHANGELOG.md updated under *Unreleased* if users would notice
|
|
17
|
+
- [ ] No real keys, customer data or confidential text anywhere in the diff
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Repository instructions
|
|
2
|
+
|
|
3
|
+
`CLAUDE.md` at the repository root is the source of truth for this repository's architecture,
|
|
4
|
+
commands and rules; read it before changing code. `CONTRIBUTING.md` has the checks and ground
|
|
5
|
+
rules. The invariants below must never be broken:
|
|
6
|
+
|
|
7
|
+
- The CLI prints only through `cli._emit()` (no trailing newline); every failure is a
|
|
8
|
+
`[promptmend: ...]` marker on stdout with exit code 0, never a traceback.
|
|
9
|
+
- Build every provider with `factory.make_provider()`, which wraps any call that can leave the
|
|
10
|
+
machine in the data-protection gate. The only overrides are `ALLOW_CLOUD_OVERRIDE=true` and
|
|
11
|
+
`--allow-flagged` (one call, soft findings only); never add a code path around the gate.
|
|
12
|
+
- Every Espanso match that runs the CLI sets `force_mode: clipboard`, and every match sets
|
|
13
|
+
`left_word: true`.
|
|
14
|
+
- Match commands start with the quoted `"__PROMPT_WORKFLOW__"` placeholder and quote nothing
|
|
15
|
+
else; nothing is ever deployed to Espanso's `config/` folder.
|
|
16
|
+
- Keep existing triggers working and every feature cross-platform (macOS and Windows); a
|
|
17
|
+
renamed or removed trigger needs a migration note and a test.
|
|
18
|
+
- Never put API keys, customer data, credentials or confidential prompts in source, tests,
|
|
19
|
+
logs or commits.
|
|
20
|
+
- Tests stay offline: never call external APIs from unit tests; mock the HTTP client.
|
|
21
|
+
- Update README.md and the CHANGELOG `## Unreleased` section with every user-visible change.
|
|
22
|
+
- Do not run the paid bench (`scripts/bench_models.py`), `pytest -m live`, the installers or
|
|
23
|
+
`espanso` commands unless asked.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Actions and pre-commit hooks are pinned to commit SHAs; Dependabot keeps those pins and
|
|
2
|
+
# uv.lock current. The cooldown skips releases younger than a week, the window in which a
|
|
3
|
+
# hijacked package is usually caught and yanked; security updates are not delayed by it.
|
|
4
|
+
version: 2
|
|
5
|
+
updates:
|
|
6
|
+
- package-ecosystem: github-actions
|
|
7
|
+
directory: /
|
|
8
|
+
schedule:
|
|
9
|
+
interval: weekly
|
|
10
|
+
cooldown:
|
|
11
|
+
default-days: 7
|
|
12
|
+
groups:
|
|
13
|
+
actions:
|
|
14
|
+
patterns: ["*"]
|
|
15
|
+
- package-ecosystem: uv
|
|
16
|
+
directory: /
|
|
17
|
+
schedule:
|
|
18
|
+
interval: weekly
|
|
19
|
+
cooldown:
|
|
20
|
+
default-days: 7
|
|
21
|
+
groups:
|
|
22
|
+
python:
|
|
23
|
+
patterns: ["*"]
|
|
24
|
+
- package-ecosystem: pre-commit
|
|
25
|
+
directory: /
|
|
26
|
+
schedule:
|
|
27
|
+
interval: weekly
|
|
28
|
+
cooldown:
|
|
29
|
+
default-days: 7
|
|
30
|
+
groups:
|
|
31
|
+
pre-commit:
|
|
32
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
# Builds the sdist, the wheel and constraints.txt (the runtime pins from uv.lock), installs the
|
|
3
|
+
# wheel with those pins into a clean venv on every OS, then attests the files and creates the
|
|
4
|
+
# annotated tag v<version> and a GitHub Release with that version's CHANGELOG notes. While the
|
|
5
|
+
# repository variable PYPI_PUBLISH is "true", the same sdist and wheel then go to PyPI through
|
|
6
|
+
# trusted publishing (the pypi job, in the GitHub environment "pypi").
|
|
7
|
+
#
|
|
8
|
+
# A release happens only for the current head of main, when its pyproject version has no
|
|
9
|
+
# published Release yet:
|
|
10
|
+
# - manual dispatch with dry-run unticked (dry-run is the default: it builds and tests, nothing more);
|
|
11
|
+
# - a push to main, only while the repository variable RELEASE_ON_PUSH is "true", so merging
|
|
12
|
+
# a branch never releases by surprise.
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
branches: [main]
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
dry-run:
|
|
19
|
+
description: "Build and test the artifacts only: no attestation, tag or Release"
|
|
20
|
+
type: boolean
|
|
21
|
+
default: true
|
|
22
|
+
permissions: {}
|
|
23
|
+
jobs:
|
|
24
|
+
plan:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
timeout-minutes: 5
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
outputs:
|
|
30
|
+
version: ${{ steps.plan.outputs.version }}
|
|
31
|
+
build: ${{ steps.plan.outputs.build }}
|
|
32
|
+
release: ${{ steps.plan.outputs.release }}
|
|
33
|
+
steps:
|
|
34
|
+
- name: Checkout
|
|
35
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
36
|
+
with:
|
|
37
|
+
persist-credentials: false
|
|
38
|
+
- name: Decide whether to release
|
|
39
|
+
id: plan
|
|
40
|
+
env:
|
|
41
|
+
GH_TOKEN: ${{ github.token }}
|
|
42
|
+
EVENT: ${{ github.event_name }}
|
|
43
|
+
DRY_RUN: ${{ inputs.dry-run }}
|
|
44
|
+
RELEASE_ON_PUSH: ${{ vars.RELEASE_ON_PUSH }}
|
|
45
|
+
run: |
|
|
46
|
+
# Fails unless the newest CHANGELOG section is the pyproject version, dated.
|
|
47
|
+
version=$(python3 scripts/release_notes.py version)
|
|
48
|
+
tag="v$version"
|
|
49
|
+
# A draft is invisible to this read-only token; the release job picks it up again.
|
|
50
|
+
if published=$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json isDraft \
|
|
51
|
+
--jq '.isDraft | not' 2> "$RUNNER_TEMP/err"); then :
|
|
52
|
+
elif grep -q "release not found" "$RUNNER_TEMP/err"; then published=false
|
|
53
|
+
else cat "$RUNNER_TEMP/err" >&2; exit 1
|
|
54
|
+
fi
|
|
55
|
+
head=$(gh api "repos/$GITHUB_REPOSITORY/commits/main" --jq .sha)
|
|
56
|
+
build=false
|
|
57
|
+
release=false
|
|
58
|
+
if [ "$EVENT" = workflow_dispatch ]; then build=true; fi
|
|
59
|
+
if [ "$published" = true ]; then
|
|
60
|
+
echo "::notice::$tag is already released; nothing to release."
|
|
61
|
+
elif [ "$GITHUB_REF" != refs/heads/main ] || [ "$GITHUB_SHA" != "$head" ]; then
|
|
62
|
+
echo "::notice::$tag is released only from the head of main ($head), not $GITHUB_REF at $GITHUB_SHA."
|
|
63
|
+
elif [ "$EVENT" = workflow_dispatch ] && [ "$DRY_RUN" = true ]; then
|
|
64
|
+
echo "::notice::Dry run: $tag is built and tested, not released."
|
|
65
|
+
elif [ "$EVENT" = workflow_dispatch ]; then
|
|
66
|
+
release=true
|
|
67
|
+
elif [ "$EVENT" = push ] && [ "$RELEASE_ON_PUSH" = true ]; then
|
|
68
|
+
build=true
|
|
69
|
+
release=true
|
|
70
|
+
else
|
|
71
|
+
echo "::notice::$tag is not released yet: run the release workflow with dry-run unticked."
|
|
72
|
+
fi
|
|
73
|
+
{
|
|
74
|
+
echo "version=$version"
|
|
75
|
+
echo "build=$build"
|
|
76
|
+
echo "release=$release"
|
|
77
|
+
} >> "$GITHUB_OUTPUT"
|
|
78
|
+
|
|
79
|
+
build:
|
|
80
|
+
needs: plan
|
|
81
|
+
if: needs.plan.outputs.build == 'true'
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
timeout-minutes: 10
|
|
84
|
+
permissions:
|
|
85
|
+
contents: read
|
|
86
|
+
steps:
|
|
87
|
+
- name: Checkout
|
|
88
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
89
|
+
with:
|
|
90
|
+
persist-credentials: false
|
|
91
|
+
# No cache in a workflow that publishes: a poisoned cache could reach the artifacts.
|
|
92
|
+
- name: Set up uv
|
|
93
|
+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
94
|
+
with:
|
|
95
|
+
python-version: "3.12"
|
|
96
|
+
enable-cache: false
|
|
97
|
+
- name: Build sdist, wheel and constraints
|
|
98
|
+
run: |
|
|
99
|
+
uv build --out-dir dist
|
|
100
|
+
uv export --frozen --no-dev --no-emit-project --no-hashes --output-file dist/constraints.txt
|
|
101
|
+
- name: Upload the artifacts
|
|
102
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
103
|
+
with:
|
|
104
|
+
name: dist
|
|
105
|
+
path: dist/
|
|
106
|
+
if-no-files-found: error
|
|
107
|
+
|
|
108
|
+
# The wheel goes into a clean venv outside the checkout, pinned by the constraints.txt being
|
|
109
|
+
# released, so the check covers what a user installs (see the wheel job in test.yml).
|
|
110
|
+
test-artifacts:
|
|
111
|
+
needs: [plan, build]
|
|
112
|
+
timeout-minutes: 10
|
|
113
|
+
permissions:
|
|
114
|
+
contents: read
|
|
115
|
+
strategy:
|
|
116
|
+
fail-fast: true
|
|
117
|
+
matrix:
|
|
118
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
119
|
+
python-version: ["3.12", "3.14"]
|
|
120
|
+
runs-on: ${{ matrix.os }}
|
|
121
|
+
defaults:
|
|
122
|
+
run:
|
|
123
|
+
shell: pwsh
|
|
124
|
+
steps:
|
|
125
|
+
- name: Checkout
|
|
126
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
127
|
+
with:
|
|
128
|
+
persist-credentials: false
|
|
129
|
+
- name: Set up uv
|
|
130
|
+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
131
|
+
with:
|
|
132
|
+
python-version: ${{ matrix.python-version }}
|
|
133
|
+
enable-cache: false
|
|
134
|
+
- name: Download the artifacts
|
|
135
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
136
|
+
with:
|
|
137
|
+
name: dist
|
|
138
|
+
path: ${{ runner.temp }}/dist
|
|
139
|
+
- name: Install the wheel with the constraints into a clean venv and check it
|
|
140
|
+
working-directory: ${{ runner.temp }}
|
|
141
|
+
env:
|
|
142
|
+
VERSION: ${{ needs.plan.outputs.version }}
|
|
143
|
+
PYTHON_VERSION: ${{ matrix.python-version }}
|
|
144
|
+
run: |
|
|
145
|
+
$PSNativeCommandUseErrorActionPreference = $true
|
|
146
|
+
$dist = Join-Path $env:RUNNER_TEMP "dist"
|
|
147
|
+
$wheel = (Get-ChildItem "$dist/*.whl" | Select-Object -First 1).FullName
|
|
148
|
+
$constraints = Join-Path $dist "constraints.txt"
|
|
149
|
+
$venv = Join-Path $env:RUNNER_TEMP "wheel-venv"
|
|
150
|
+
uv venv --python $env:PYTHON_VERSION $venv
|
|
151
|
+
uv pip install --python $venv $wheel -c $constraints
|
|
152
|
+
$python = if ($IsWindows) { "$venv/Scripts/python.exe" } else { "$venv/bin/python" }
|
|
153
|
+
$cli = if ($IsWindows) { "$venv/Scripts/promptmend.exe" } else { "$venv/bin/promptmend" }
|
|
154
|
+
$installed = & $cli --version
|
|
155
|
+
if ($installed -ne $env:VERSION) { throw "installed $installed, expected $env:VERSION" }
|
|
156
|
+
& $python "$env:GITHUB_WORKSPACE/scripts/check_wheel.py" --repo $env:GITHUB_WORKSPACE --wheel $wheel --constraints $constraints
|
|
157
|
+
|
|
158
|
+
release:
|
|
159
|
+
needs: [plan, build, test-artifacts]
|
|
160
|
+
if: needs.plan.outputs.release == 'true'
|
|
161
|
+
runs-on: ubuntu-latest
|
|
162
|
+
timeout-minutes: 10
|
|
163
|
+
# One release at a time, never cancelled half way between the tag and the Release.
|
|
164
|
+
concurrency:
|
|
165
|
+
group: release
|
|
166
|
+
cancel-in-progress: false
|
|
167
|
+
# The only job that can write to the repository: the tag, the Release and the attestations.
|
|
168
|
+
permissions:
|
|
169
|
+
contents: write
|
|
170
|
+
id-token: write
|
|
171
|
+
attestations: write
|
|
172
|
+
steps:
|
|
173
|
+
- name: Checkout
|
|
174
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
175
|
+
with:
|
|
176
|
+
persist-credentials: false
|
|
177
|
+
- name: Download the artifacts
|
|
178
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
179
|
+
with:
|
|
180
|
+
name: dist
|
|
181
|
+
path: dist
|
|
182
|
+
- name: Attest build provenance
|
|
183
|
+
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
|
184
|
+
with:
|
|
185
|
+
subject-path: dist/*
|
|
186
|
+
# The tag is made through the API, so no git credentials are stored in the checkout.
|
|
187
|
+
# The Release starts as a draft and is published once every file is attached: with
|
|
188
|
+
# immutable releases enabled in the repository, publishing is what locks it. A run that
|
|
189
|
+
# failed half way can be re-run: an annotated tag already on this commit and a draft
|
|
190
|
+
# Release are reused.
|
|
191
|
+
- name: Create the annotated tag and the Release
|
|
192
|
+
env:
|
|
193
|
+
GH_TOKEN: ${{ github.token }}
|
|
194
|
+
VERSION: ${{ needs.plan.outputs.version }}
|
|
195
|
+
run: |
|
|
196
|
+
tag="v$VERSION"
|
|
197
|
+
repo="$GITHUB_REPOSITORY"
|
|
198
|
+
head=$(gh api "repos/$repo/commits/main" --jq .sha)
|
|
199
|
+
if [ "$GITHUB_SHA" != "$head" ]; then
|
|
200
|
+
echo "::error::main moved on to $head while $GITHUB_SHA was tested; not releasing."
|
|
201
|
+
exit 1
|
|
202
|
+
fi
|
|
203
|
+
python3 scripts/release_notes.py notes > "$RUNNER_TEMP/notes.md"
|
|
204
|
+
|
|
205
|
+
ref=$(gh api "repos/$repo/git/matching-refs/tags/$tag" \
|
|
206
|
+
--jq "map(select(.ref == \"refs/tags/$tag\")) | .[0] // empty | .object.type + \" \" + .object.sha")
|
|
207
|
+
if [ -z "$ref" ]; then
|
|
208
|
+
tag_sha=$(gh api "repos/$repo/git/tags" -f tag="$tag" -f message="Release $VERSION" \
|
|
209
|
+
-f object="$GITHUB_SHA" -f type=commit --jq .sha)
|
|
210
|
+
gh api "repos/$repo/git/refs" -f ref="refs/tags/$tag" -f sha="$tag_sha" > /dev/null
|
|
211
|
+
else
|
|
212
|
+
read -r type sha <<< "$ref"
|
|
213
|
+
target=""
|
|
214
|
+
if [ "$type" = tag ]; then target=$(gh api "repos/$repo/git/tags/$sha" --jq .object.sha); fi
|
|
215
|
+
if [ "$target" != "$GITHUB_SHA" ]; then
|
|
216
|
+
echo "::error::$tag already exists as a $type ($sha), not an annotated tag of $GITHUB_SHA."
|
|
217
|
+
exit 1
|
|
218
|
+
fi
|
|
219
|
+
echo "::notice::Reusing the annotated tag $tag."
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
if draft=$(gh release view "$tag" --repo "$repo" --json isDraft --jq .isDraft 2> "$RUNNER_TEMP/err"); then :
|
|
223
|
+
elif grep -q "release not found" "$RUNNER_TEMP/err"; then draft=none
|
|
224
|
+
else cat "$RUNNER_TEMP/err" >&2; exit 1
|
|
225
|
+
fi
|
|
226
|
+
case "$draft" in
|
|
227
|
+
none)
|
|
228
|
+
gh release create "$tag" --repo "$repo" --verify-tag --draft \
|
|
229
|
+
--title "$tag" --notes-file "$RUNNER_TEMP/notes.md" dist/* ;;
|
|
230
|
+
true)
|
|
231
|
+
echo "::notice::Reusing the draft Release $tag."
|
|
232
|
+
gh release edit "$tag" --repo "$repo" --title "$tag" --notes-file "$RUNNER_TEMP/notes.md"
|
|
233
|
+
gh release upload "$tag" --repo "$repo" --clobber dist/* ;;
|
|
234
|
+
*)
|
|
235
|
+
echo "::error::$tag is already published."
|
|
236
|
+
exit 1 ;;
|
|
237
|
+
esac
|
|
238
|
+
|
|
239
|
+
# Latest only if no published Release has a higher version.
|
|
240
|
+
newest=$( { gh release list --repo "$repo" --exclude-drafts --limit 1000 \
|
|
241
|
+
--json tagName --jq '.[].tagName'; echo "$tag"; } | sort -V | tail -n 1)
|
|
242
|
+
latest=false
|
|
243
|
+
if [ "$newest" = "$tag" ]; then latest=true; fi
|
|
244
|
+
gh release edit "$tag" --repo "$repo" --draft=false --latest="$latest"
|
|
245
|
+
|
|
246
|
+
# PyPI, after the Release is published, with the very sdist and wheel the release job attested
|
|
247
|
+
# (the artifact of this run, never a rebuild). Off until the owner sets PYPI_PUBLISH to "true"
|
|
248
|
+
# and registers this workflow and the environment "pypi" as a trusted publisher on PyPI, so a
|
|
249
|
+
# release keeps working before that. It holds no repository permission, only the OIDC token
|
|
250
|
+
# PyPI exchanges for a short-lived upload token; no API token is stored anywhere. If it fails,
|
|
251
|
+
# "Re-run failed jobs" uploads the same files again (skip-existing: files PyPI already has
|
|
252
|
+
# are left alone). A fresh run for a published version stops at plan, so it never re-uploads.
|
|
253
|
+
pypi:
|
|
254
|
+
needs: [plan, release]
|
|
255
|
+
if: needs.plan.outputs.release == 'true' && vars.PYPI_PUBLISH == 'true'
|
|
256
|
+
runs-on: ubuntu-latest
|
|
257
|
+
timeout-minutes: 10
|
|
258
|
+
concurrency:
|
|
259
|
+
group: pypi
|
|
260
|
+
cancel-in-progress: false
|
|
261
|
+
environment:
|
|
262
|
+
name: pypi
|
|
263
|
+
url: https://pypi.org/project/promptmend/${{ needs.plan.outputs.version }}/
|
|
264
|
+
permissions:
|
|
265
|
+
id-token: write
|
|
266
|
+
steps:
|
|
267
|
+
- name: Download the artifacts
|
|
268
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
269
|
+
with:
|
|
270
|
+
name: dist
|
|
271
|
+
path: ${{ runner.temp }}/artifacts
|
|
272
|
+
# Exactly the sdist and the wheel of this version; constraints.txt stays on the Release.
|
|
273
|
+
- name: Pick the sdist and the wheel
|
|
274
|
+
env:
|
|
275
|
+
VERSION: ${{ needs.plan.outputs.version }}
|
|
276
|
+
run: |
|
|
277
|
+
mkdir "$RUNNER_TEMP/pypi"
|
|
278
|
+
for name in "promptmend-$VERSION.tar.gz" \
|
|
279
|
+
"promptmend-$VERSION-py3-none-any.whl"; do
|
|
280
|
+
cp "$RUNNER_TEMP/artifacts/$name" "$RUNNER_TEMP/pypi/"
|
|
281
|
+
done
|
|
282
|
+
ls -l "$RUNNER_TEMP/pypi"
|
|
283
|
+
- name: Publish to PyPI
|
|
284
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
285
|
+
with:
|
|
286
|
+
packages-dir: ${{ runner.temp }}/pypi
|
|
287
|
+
skip-existing: true
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: secret-scan
|
|
2
|
+
on:
|
|
3
|
+
# Every branch: a secret pushed anywhere is already public.
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
# gitleaks-action lists a pull request's commits to scan only those.
|
|
9
|
+
pull-requests: read
|
|
10
|
+
concurrency:
|
|
11
|
+
group: secret-scan-${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
jobs:
|
|
14
|
+
gitleaks:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 10
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
- name: Run gitleaks
|
|
23
|
+
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
|
|
24
|
+
env:
|
|
25
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|