pipulate 2.73__tar.gz → 2.74__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.
- {pipulate-2.73 → pipulate-2.74}/.agents/skills/journal/references/index.md +8 -5
- {pipulate-2.73 → pipulate-2.74}/PKG-INFO +8 -8
- {pipulate-2.73 → pipulate-2.74}/README.md +7 -7
- {pipulate-2.73 → pipulate-2.74}/__init__.py +2 -2
- {pipulate-2.73 → pipulate-2.74}/imports/voice_synthesis.py +172 -11
- {pipulate-2.73 → pipulate-2.74}/pipulate.egg-info/PKG-INFO +8 -8
- {pipulate-2.73 → pipulate-2.74}/pyproject.toml +1 -1
- {pipulate-2.73 → pipulate-2.74}/.agents/skills/gsc-readonly/SKILL.md +0 -0
- {pipulate-2.73 → pipulate-2.74}/.agents/skills/hello-workflow/SKILL.md +0 -0
- {pipulate-2.73 → pipulate-2.74}/.agents/skills/journal/SKILL.md +0 -0
- {pipulate-2.73 → pipulate-2.74}/.agents/skills/pipulate/SKILL.md +0 -0
- {pipulate-2.73 → pipulate-2.74}/.agents/skills/roles/SKILL.md +0 -0
- {pipulate-2.73 → pipulate-2.74}/.agents/skills/sheets-readonly/SKILL.md +0 -0
- {pipulate-2.73 → pipulate-2.74}/AGENTS.md +0 -0
- {pipulate-2.73 → pipulate-2.74}/AUDIT.md +0 -0
- {pipulate-2.73 → pipulate-2.74}/LICENSE +0 -0
- {pipulate-2.73 → pipulate-2.74}/MANIFEST.in +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/010_introduction.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/015_config.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/020_profiles.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/025_aspect.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/030_roles.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/040_hello_workflow.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/050_documentation.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/060_tasks.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/070_history.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/110_parameter_buster.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/120_link_graph.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/130_gap_analysis.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/200_workflow_genesis.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/210_widget_examples.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/220_roadmap.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/230_dev_assistant.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/240_simon_mcp.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/300_blank_placeholder.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/400_botify_trifecta.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/440_browser_automation.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/450_stream_simulator.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/510_text_field.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/520_text_area.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/530_dropdown.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/540_checkboxes.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/550_radios.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/560_range.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/570_switch.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/580_upload.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/610_markdown.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/620_mermaid.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/630_prism.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/640_javascript.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/710_pandas.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/720_rich.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/730_matplotlib.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/810_webbrowser.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/820_selenium.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/apps/830_pico_slider.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/cli.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/config.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/__init__.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/ai_dictdb.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/ai_tool_discovery_simple_parser.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/append_only_conversation.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/ascii_displays.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/botify/__init__.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/botify/code_generators.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/botify/true_schema_discoverer.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/botify_code_generation.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/crud.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/database_safety_wrapper.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/dom_processing/__init__.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/dom_processing/ai_dom_beautifier.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/dom_processing/enhanced_dom_processor.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/durable_backup_system.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/mcp_orchestrator.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/server_logging.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/imports/stream_orchestrator.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/pipulate/__init__.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/pipulate/core.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/pipulate.egg-info/SOURCES.txt +0 -0
- {pipulate-2.73 → pipulate-2.74}/pipulate.egg-info/dependency_links.txt +0 -0
- {pipulate-2.73 → pipulate-2.74}/pipulate.egg-info/entry_points.txt +0 -0
- {pipulate-2.73 → pipulate-2.74}/pipulate.egg-info/requires.txt +0 -0
- {pipulate-2.73 → pipulate-2.74}/pipulate.egg-info/top_level.txt +0 -0
- {pipulate-2.73 → pipulate-2.74}/server.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/setup.cfg +0 -0
- {pipulate-2.73 → pipulate-2.74}/tests/test_apply_airlock.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tests/test_mck_rep2.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/__init__.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/advanced_automation_tools.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/botify_tools.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/connector_tools.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/conversation_tools.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/dom_tools.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/keychain_tools.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/llm_optics.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/mcp_tools.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/scraper_tools.py +0 -0
- {pipulate-2.73 → pipulate-2.74}/tools/system_tools.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# The Pipulate journal, indexed
|
|
2
2
|
|
|
3
|
-
> Auto-generated on 2026-
|
|
3
|
+
> Auto-generated on 2026-10-01 by `scripts/articles/generate_ai_context.py` and
|
|
4
4
|
> rewritten from scratch on every release. If this date looks stale, assume
|
|
5
|
-
> the rest of the repo is newer than this map.
|
|
5
|
+
> the rest of the repo is newer than this map. 1508 entries indexed.
|
|
6
6
|
|
|
7
7
|
This repository holds the *machinery*. The *reasoning*, the running journal
|
|
8
8
|
that explains why every piece exists, lives on a separate website and not in
|
|
@@ -27,6 +27,9 @@ The first 20 entries include full `index.md` URLs to establish
|
|
|
27
27
|
the link pattern. All remaining entries are bare slugs. Reconstruct any full
|
|
28
28
|
URL as: `https://mikelev.in/futureproof/{slug}/index.md`
|
|
29
29
|
|
|
30
|
+
- [2026-10-01] [The Scrollback Rule: Engineering Full-Width Terminal Dividers for Replayable Logs](https://mikelev.in/futureproof/scrollback-rule-clear-x-receipts/index.md)
|
|
31
|
+
- [2026-10-01] [The Prefix Ladder and the Unalias Guard: Replayable Shell Ergonomics in the Age of AI](https://mikelev.in/futureproof/prefix-ladders-and-the-unalias-guard/index.md)
|
|
32
|
+
- [2026-09-30] [Forever-Commands and the Talk Toggle: Replayable CLI Habits in the Age of AI](https://mikelev.in/futureproof/forever-commands-and-the-talk-toggle/index.md)
|
|
30
33
|
- [2026-09-29] [Layered Reality and Operator Overloading: Reading the Physics of CPython in the Age of AI](https://mikelev.in/futureproof/layered-reality-operator-overloading-cpython/index.md)
|
|
31
34
|
- [2026-09-29] [The Three-Door Installer: Named Prompts, Split DNS, and Verifiable Releases](https://mikelev.in/futureproof/the-three-door-installer-and-named-prompts/index.md)
|
|
32
35
|
- [2026-09-29] [The Deed Is Named for the Door: Engineering Replayable QA Archives for AI](https://mikelev.in/futureproof/the-deed-is-named-for-the-door-qa-archive-zip/index.md)
|
|
@@ -44,14 +47,14 @@ URL as: `https://mikelev.in/futureproof/{slug}/index.md`
|
|
|
44
47
|
- [2026-09-20] [The Letting-Go Moment: Building Vim Training Wheels for the Age of AI](https://mikelev.in/futureproof/vim-training-wheels-and-the-letting-go-moment/index.md)
|
|
45
48
|
- [2026-09-20] [Disciplined Forgetting: Scaling AI Context Through a Two-Tier Memory Hierarchy](https://mikelev.in/futureproof/disciplined-forgetting-and-the-memory-hierarchy/index.md)
|
|
46
49
|
- [2026-09-19] [Four Models, One Matchbook: Multi-Model Consensus in the Age of AI](https://mikelev.in/futureproof/four-models-one-matchbook-prompt-fanout/index.md)
|
|
47
|
-
- [2026-09-19] [Dual-Model Failover: Engineering Independent Backoff Clocks for LLM APIs](https://mikelev.in/futureproof/dual-model-failover-independent-backoff/index.md)
|
|
48
|
-
- [2026-09-19] [Playing Pinball in the Age of AI: A Replayable Workflow Guide](https://mikelev.in/futureproof/playing-pinball-in-the-age-of-ai-replayable-workflow-guide/index.md)
|
|
49
|
-
- [2026-09-19] [The Color Blue and the Pipe: Inspecting Code Before Execution](https://mikelev.in/futureproof/color-blue-pipe-inspecting-code/index.md)
|
|
50
50
|
|
|
51
51
|
## Compact slug index -- pattern: https://mikelev.in/futureproof/{slug}/index.md
|
|
52
52
|
|
|
53
53
|
Format: `[date] [size] slug` -- fetch any entry as `https://mikelev.in/futureproof/{slug}/index.md`
|
|
54
54
|
|
|
55
|
+
- [2026-09-19] [48k] dual-model-failover-independent-backoff
|
|
56
|
+
- [2026-09-19] [193k] playing-pinball-in-the-age-of-ai-replayable-workflow-guide
|
|
57
|
+
- [2026-09-19] [72k] color-blue-pipe-inspecting-code
|
|
55
58
|
- [2026-09-19] [294k] edna-mode-rules-for-ai-workflows
|
|
56
59
|
- [2026-09-18] [207k] the-one-door-threshold-replayable-ai-workflows
|
|
57
60
|
- [2026-09-18] [292k] the-first-ten-minutes-reproducible-ai-workflows
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipulate
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.74
|
|
4
4
|
Summary: The context compiler that helps you prompt well -- local-first, Nix-reproducible, replayable. The successor to AI SEO software.
|
|
5
5
|
Author-email: Mike Levin <pipulate@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -86,7 +86,7 @@ Under both sits `flake.nix` with its `flake.lock`: the dependency inventory an S
|
|
|
86
86
|
|
|
87
87
|
## Quick Start: Be Running in 5 Minutes
|
|
88
88
|
|
|
89
|
-
**Want to skip the philosophy and just see what this does?
|
|
89
|
+
**Want to skip the philosophy and just see what this does? Open the command line (on a Mac, the app called Terminal) and paste this:**
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
curl -fsSL https://pipulate.com/install.sh | bash
|
|
@@ -113,8 +113,8 @@ These few commands:
|
|
|
113
113
|
|
|
114
114
|
### 4. Run It Again!
|
|
115
115
|
|
|
116
|
-
1. You can just
|
|
117
|
-
2. Open a new
|
|
116
|
+
1. You can just close the window it's running in.
|
|
117
|
+
2. Open a new one, and once again:
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
120
|
cd ~/pipulate
|
|
@@ -242,7 +242,7 @@ Pipulate is a **local-first, single-tenant desktop app framework** featuring AI-
|
|
|
242
242
|
│ │
|
|
243
243
|
▼ ▼
|
|
244
244
|
┌─────────────────────────┐ ┌─────────────────────────┐ This is new.
|
|
245
|
-
│ 📱 Native Window │ │ 🖥️
|
|
245
|
+
│ 📱 Native Window │ │ 🖥️ The Command Line │ , O
|
|
246
246
|
│ ┌─────────────────┐ │ │ ┌─────────────────────┐ │ \\ . O
|
|
247
247
|
│ │ Web Browser │ │ │ │ nix develop (./run) │ │ |\\/| o
|
|
248
248
|
│ │ (Bundled) │ │ │ │ Starting servers... │ │ / " '\
|
|
@@ -628,7 +628,7 @@ Type `jn` at the `(nix)` prompt, wait for the JupyterLab tab to open, then run t
|
|
|
628
628
|
|
|
629
629
|
| Problem | Solution |
|
|
630
630
|
|---------|----------|
|
|
631
|
-
| `nix: command not found` |
|
|
631
|
+
| `nix: command not found` | Close the window and open a new one; Nix needs a fresh window after it installs |
|
|
632
632
|
| Browser doesn't open automatically | Manually visit `http://localhost:5001` and `http://localhost:8888` |
|
|
633
633
|
| `Permission denied` errors | Make sure you can write to `~/pipulate` directory |
|
|
634
634
|
| Port conflicts | Kill processes on ports 5001/8888: `lsof -ti:5001 \| xargs kill -9` |
|
|
@@ -656,7 +656,7 @@ If you are a developer comfortable with tools like Homebrew and `pipx`, you can
|
|
|
656
656
|
brew install pipx
|
|
657
657
|
```
|
|
658
658
|
|
|
659
|
-
**Step 2: Install the
|
|
659
|
+
**Step 2: Install the `pipulate` command**
|
|
660
660
|
|
|
661
661
|
Use `pipx` to install the `pipulate` command-line tool. This will not cause conflicts with your system Python.
|
|
662
662
|
|
|
@@ -871,7 +871,7 @@ Traditional development follows DRY principles, creating abstract, complex syste
|
|
|
871
871
|
|
|
872
872
|
## Developer Setup & Environment Notes
|
|
873
873
|
|
|
874
|
-
**Nix Environment Activation:** Always run `nix develop` from the `~/pipulate` directory *before* running any project commands (`python server.py`, `pip install`, etc.) in a new
|
|
874
|
+
**Nix Environment Activation:** Always run `nix develop` from the `~/pipulate` directory *before* running any project commands (`python server.py`, `pip install`, etc.) in a new window. This ensures you are using the correct dependencies defined in `flake.nix`.
|
|
875
875
|
|
|
876
876
|
**Interactive vs. Quiet Shell:**
|
|
877
877
|
|
|
@@ -29,7 +29,7 @@ Under both sits `flake.nix` with its `flake.lock`: the dependency inventory an S
|
|
|
29
29
|
|
|
30
30
|
## Quick Start: Be Running in 5 Minutes
|
|
31
31
|
|
|
32
|
-
**Want to skip the philosophy and just see what this does?
|
|
32
|
+
**Want to skip the philosophy and just see what this does? Open the command line (on a Mac, the app called Terminal) and paste this:**
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
curl -fsSL https://pipulate.com/install.sh | bash
|
|
@@ -56,8 +56,8 @@ These few commands:
|
|
|
56
56
|
|
|
57
57
|
### 4. Run It Again!
|
|
58
58
|
|
|
59
|
-
1. You can just
|
|
60
|
-
2. Open a new
|
|
59
|
+
1. You can just close the window it's running in.
|
|
60
|
+
2. Open a new one, and once again:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
63
|
cd ~/pipulate
|
|
@@ -185,7 +185,7 @@ Pipulate is a **local-first, single-tenant desktop app framework** featuring AI-
|
|
|
185
185
|
│ │
|
|
186
186
|
▼ ▼
|
|
187
187
|
┌─────────────────────────┐ ┌─────────────────────────┐ This is new.
|
|
188
|
-
│ 📱 Native Window │ │ 🖥️
|
|
188
|
+
│ 📱 Native Window │ │ 🖥️ The Command Line │ , O
|
|
189
189
|
│ ┌─────────────────┐ │ │ ┌─────────────────────┐ │ \\ . O
|
|
190
190
|
│ │ Web Browser │ │ │ │ nix develop (./run) │ │ |\\/| o
|
|
191
191
|
│ │ (Bundled) │ │ │ │ Starting servers... │ │ / " '\
|
|
@@ -571,7 +571,7 @@ Type `jn` at the `(nix)` prompt, wait for the JupyterLab tab to open, then run t
|
|
|
571
571
|
|
|
572
572
|
| Problem | Solution |
|
|
573
573
|
|---------|----------|
|
|
574
|
-
| `nix: command not found` |
|
|
574
|
+
| `nix: command not found` | Close the window and open a new one; Nix needs a fresh window after it installs |
|
|
575
575
|
| Browser doesn't open automatically | Manually visit `http://localhost:5001` and `http://localhost:8888` |
|
|
576
576
|
| `Permission denied` errors | Make sure you can write to `~/pipulate` directory |
|
|
577
577
|
| Port conflicts | Kill processes on ports 5001/8888: `lsof -ti:5001 \| xargs kill -9` |
|
|
@@ -599,7 +599,7 @@ If you are a developer comfortable with tools like Homebrew and `pipx`, you can
|
|
|
599
599
|
brew install pipx
|
|
600
600
|
```
|
|
601
601
|
|
|
602
|
-
**Step 2: Install the
|
|
602
|
+
**Step 2: Install the `pipulate` command**
|
|
603
603
|
|
|
604
604
|
Use `pipx` to install the `pipulate` command-line tool. This will not cause conflicts with your system Python.
|
|
605
605
|
|
|
@@ -814,7 +814,7 @@ Traditional development follows DRY principles, creating abstract, complex syste
|
|
|
814
814
|
|
|
815
815
|
## Developer Setup & Environment Notes
|
|
816
816
|
|
|
817
|
-
**Nix Environment Activation:** Always run `nix develop` from the `~/pipulate` directory *before* running any project commands (`python server.py`, `pip install`, etc.) in a new
|
|
817
|
+
**Nix Environment Activation:** Always run `nix develop` from the `~/pipulate` directory *before* running any project commands (`python server.py`, `pip install`, etc.) in a new window. This ensures you are using the correct dependencies defined in `flake.nix`.
|
|
818
818
|
|
|
819
819
|
**Interactive vs. Quiet Shell:**
|
|
820
820
|
|
|
@@ -12,7 +12,7 @@ Usage:
|
|
|
12
12
|
pipulate
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
__version__ = "2.
|
|
15
|
+
__version__ = "2.74"
|
|
16
16
|
# APOSTROPHES RESTORED (2026-08-04). They were stripped as a workaround for
|
|
17
17
|
# flake.nix's descMatch regex, whose character class excluded ' from the
|
|
18
18
|
# CAPTURE and truncated the banner to "(So)". That regex was fixed in the same
|
|
@@ -23,7 +23,7 @@ __version__ = "2.73"
|
|
|
23
23
|
# is not a property of the system; the regex is. Blast radius is one banner:
|
|
24
24
|
# nothing but flake.nix reads this name -- version_sync.py syncs __version__
|
|
25
25
|
# and __description__, never this. So'wI' chu' -- "engage the cloaking device."
|
|
26
|
-
__version_description__ = "
|
|
26
|
+
__version_description__ = "Doorways"
|
|
27
27
|
# SPDX expression, single source of truth, synced into pyproject.toml by
|
|
28
28
|
# scripts/release/version_sync.py. "-or-later" (not bare AGPL-3.0, which is
|
|
29
29
|
# deprecated SPDX) because the header below grants "any later version".
|
|
@@ -92,6 +92,13 @@ def _quiet_hub():
|
|
|
92
92
|
# to fetch the voice and spend seconds before anyone had asked to speak or
|
|
93
93
|
# been asked whether it may; that was also the import tax the tool roster
|
|
94
94
|
# measured at ~3.8 s.
|
|
95
|
+
# AMENDED 2026-10-01 (THE VOICE ASKS ONCE, OUT LOUD): first_contact() below,
|
|
96
|
+
# run once per machine right before the first menu, prints what the voice is,
|
|
97
|
+
# fetches the model with setup_voice_model's own one-line disclosure, and lets
|
|
98
|
+
# the voice ask the question itself, because a voice nobody has heard is a
|
|
99
|
+
# voice nobody can answer for (CONSENT CANNOT PRECEDE ITS OBJECT). The download
|
|
100
|
+
# is disclosed, never asked, and still never at import. The walk's card asks
|
|
101
|
+
# before it downloads, as before, and stays quiet once any answer is recorded.
|
|
95
102
|
# ---------------------------------------------------------------------------
|
|
96
103
|
VOICE_CONSENT_FILE = Path.home() / ".config" / "pipulate" / "voice"
|
|
97
104
|
VOICE_CARD = (
|
|
@@ -120,7 +127,7 @@ def voice_consent() -> str:
|
|
|
120
127
|
return "unset"
|
|
121
128
|
|
|
122
129
|
|
|
123
|
-
def ask_voice_consent(later_hint: str = "
|
|
130
|
+
def ask_voice_consent(later_hint: str = "talk", force: bool = False) -> str:
|
|
124
131
|
"""Ask on /dev/tty and record the answer. Returns 'yes', 'no' or 'unavailable'.
|
|
125
132
|
|
|
126
133
|
Asks only when nothing is recorded, unless force is set (the `voice` word),
|
|
@@ -150,15 +157,155 @@ def ask_voice_consent(later_hint: str = "voice", force: bool = False) -> str:
|
|
|
150
157
|
finally:
|
|
151
158
|
tty.close()
|
|
152
159
|
decision = "yes" if answer.strip().lower() in ("y", "yes") else "no"
|
|
160
|
+
error = _record_voice_answer(decision)
|
|
161
|
+
if error is not None:
|
|
162
|
+
print(f"(could not record the answer, so it will be asked again: {error})")
|
|
163
|
+
if decision == "no":
|
|
164
|
+
print(f"Staying quiet. To turn the voice on later, type: {later_hint}")
|
|
165
|
+
return decision
|
|
166
|
+
|
|
167
|
+
TALK_ON_WORDS = ("on", "yes", "y", "1", "true")
|
|
168
|
+
TALK_OFF_WORDS = ("off", "no", "n", "0", "false")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _record_voice_answer(decision: str):
|
|
172
|
+
"""Write 'yes' or 'no' as the recorded answer. Returns None, or the OSError.
|
|
173
|
+
|
|
174
|
+
THE ONE WRITER (2026-09-30): the card and `talk` both record here, so the
|
|
175
|
+
format voice_consent() reads has one spelling, the word and a newline.
|
|
176
|
+
"""
|
|
153
177
|
try:
|
|
154
178
|
VOICE_CONSENT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
155
179
|
VOICE_CONSENT_FILE.write_text(decision + "\n", encoding="utf-8")
|
|
156
180
|
except OSError as exc:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
return exc
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def talk_command(args) -> int:
|
|
186
|
+
"""`talk` flips narration; `talk on` and `talk off` set it. Returns an exit code.
|
|
187
|
+
|
|
188
|
+
THE TALK FLIP (2026-09-30). The recorded answer is one word, yes or no,
|
|
189
|
+
in VOICE_CONSENT_FILE, and voice_consent() reads it fresh on every call,
|
|
190
|
+
so a flip holds at the next spoken line in every process, a running
|
|
191
|
+
server included, with nothing restarted. Nothing recorded runs the card
|
|
192
|
+
first, whatever the argument: the card carries the download disclosure,
|
|
193
|
+
and talk writes only over an answer the card has recorded, so a flip to
|
|
194
|
+
on never downloads the voice undisclosed. PIPULATE_VOICE set to a word
|
|
195
|
+
voice_consent() reads outranks the file, so talk names it and writes
|
|
196
|
+
nothing. The state printed is read back through the barrier after the
|
|
197
|
+
write, never inferred from the request. Exit 0 when a state was
|
|
198
|
+
printed, 1 when nothing could be recorded, 2 for a word talk does not
|
|
199
|
+
know.
|
|
200
|
+
"""
|
|
201
|
+
words = [arg.strip().lower() for arg in args]
|
|
202
|
+
if len(words) > 1 or (words and words[0] not in TALK_ON_WORDS + TALK_OFF_WORDS):
|
|
203
|
+
print("usage: talk [on|off] (bare talk flips narration)")
|
|
204
|
+
return 2
|
|
205
|
+
want = None
|
|
206
|
+
if words:
|
|
207
|
+
want = "yes" if words[0] in TALK_ON_WORDS else "no"
|
|
208
|
+
raw = os.environ.get("PIPULATE_VOICE", "").strip()
|
|
209
|
+
if raw.lower() in ("0", "no", "off", "false", "1", "yes", "on", "true"):
|
|
210
|
+
state = "on" if voice_consent() == "yes" else "off"
|
|
211
|
+
print(f"narration: {state} (PIPULATE_VOICE={raw!r} decides in this shell, so the "
|
|
212
|
+
"recorded answer was not read or changed; unset it to use talk)")
|
|
213
|
+
return 0
|
|
214
|
+
if raw:
|
|
215
|
+
print(f"(PIPULATE_VOICE={raw!r} is not a word the speaker reads; the recorded answer decides)")
|
|
216
|
+
error = None
|
|
217
|
+
recorded = voice_consent()
|
|
218
|
+
if recorded == "unset":
|
|
219
|
+
if ask_voice_consent(later_hint="talk") == "unavailable":
|
|
220
|
+
print("talk: nothing is recorded yet and there is no keyboard to ask on; type talk at the command line.")
|
|
221
|
+
return 1
|
|
222
|
+
else:
|
|
223
|
+
decision = want or ("no" if recorded == "yes" else "yes")
|
|
224
|
+
if decision != recorded:
|
|
225
|
+
error = _record_voice_answer(decision)
|
|
226
|
+
if error is not None:
|
|
227
|
+
print(f"talk: could not record the answer: {error}")
|
|
228
|
+
after = voice_consent()
|
|
229
|
+
if after == "unset":
|
|
230
|
+
print("talk: no answer is recorded, so narration stays off.")
|
|
231
|
+
return 1
|
|
232
|
+
state = "on" if after == "yes" else "off"
|
|
233
|
+
note = ", unchanged" if after == recorded else ""
|
|
234
|
+
print(f"narration: {state}{note} (recorded in {VOICE_CONSENT_FILE})")
|
|
235
|
+
if after == "yes" and chip_voice_system is not None:
|
|
236
|
+
result = chip_voice_system.speak_text("This is Piper, reading a script. Narration is on.")
|
|
237
|
+
if not result.get("success"):
|
|
238
|
+
print(f"talk: nothing was heard: {result.get('error')}")
|
|
239
|
+
return 1 if error is not None else 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
FIRST_CONTACT_INTRO = ("This workshop can read its steps aloud with Piper, "
|
|
243
|
+
"a small voice that runs on this computer.")
|
|
244
|
+
FIRST_CONTACT_LINE = ("This is Piper, reading a script. If you can hear this, "
|
|
245
|
+
"press Enter. Type n to keep it quiet.")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def first_contact() -> str:
|
|
249
|
+
"""Ask by voice, once per machine, right before the first menu.
|
|
250
|
+
|
|
251
|
+
THE VOICE ASKS ONCE, OUT LOUD (2026-10-01). flake.nix's runScript calls
|
|
252
|
+
this before the menu prints, only where no answer is recorded. One line
|
|
253
|
+
says what the voice is; setup_voice_model prints its own 60 MB line before
|
|
254
|
+
it fetches a model that is absent; then the voice speaks FIRST_CONTACT_LINE,
|
|
255
|
+
printed at the same moment, and one answer is read from the keyboard.
|
|
256
|
+
Enter keeps the voice and n turns it off. A voice that cannot load or play
|
|
257
|
+
records no and says why in one line, so the question does not repeat on
|
|
258
|
+
every entry and talk tries again later. Ctrl+C or an empty read records
|
|
259
|
+
nothing, so the question returns next time. The one line spoken before any
|
|
260
|
+
answer exists is the question itself, which is why synthesize_and_play
|
|
261
|
+
takes ceremony=True from here and from nowhere else. Returns env,
|
|
262
|
+
recorded, unavailable, silent, interrupted, yes or no.
|
|
263
|
+
"""
|
|
264
|
+
if os.environ.get("PIPULATE_VOICE", "").strip():
|
|
265
|
+
return "env"
|
|
266
|
+
if voice_consent() != "unset":
|
|
267
|
+
return "recorded"
|
|
268
|
+
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
269
|
+
return "unavailable"
|
|
270
|
+
logger.setLevel(logging.CRITICAL)
|
|
271
|
+
voice = chip_voice_system
|
|
272
|
+
try:
|
|
273
|
+
print(FIRST_CONTACT_INTRO, flush=True)
|
|
274
|
+
played = False
|
|
275
|
+
if voice is not None and voice.ensure_voice():
|
|
276
|
+
print(FIRST_CONTACT_LINE, flush=True)
|
|
277
|
+
played = voice.synthesize_and_play(FIRST_CONTACT_LINE, ceremony=True)
|
|
278
|
+
if not played:
|
|
279
|
+
reason = (voice.last_error if voice is not None else None) or "the voice did not load"
|
|
280
|
+
reason = reason.splitlines()[0][:120]
|
|
281
|
+
error = _record_voice_answer("no")
|
|
282
|
+
print(f"The voice could not play here ({reason}), so it stays off. Type talk to try again.")
|
|
283
|
+
if error is not None:
|
|
284
|
+
print(f"(could not record that, so this runs again next time: {error})")
|
|
285
|
+
return "silent"
|
|
286
|
+
with open("/dev/tty", "r", encoding="utf-8") as tty:
|
|
287
|
+
print("Enter or n: ", end="", flush=True)
|
|
288
|
+
answer = tty.readline()
|
|
289
|
+
except KeyboardInterrupt:
|
|
290
|
+
print("\nNothing recorded; the question comes back next time.")
|
|
291
|
+
return "interrupted"
|
|
292
|
+
except OSError as exc:
|
|
293
|
+
print(f"\nNothing recorded ({exc}); the question comes back next time.")
|
|
294
|
+
return "interrupted"
|
|
295
|
+
if not answer:
|
|
296
|
+
print("\nNothing recorded; the question comes back next time.")
|
|
297
|
+
return "interrupted"
|
|
298
|
+
decision = "no" if answer.strip().lower().startswith("n") else "yes"
|
|
299
|
+
error = _record_voice_answer(decision)
|
|
300
|
+
if error is not None:
|
|
301
|
+
print(f"(could not record the answer, so it will be asked again: {error})")
|
|
302
|
+
if decision == "yes":
|
|
303
|
+
print("Voice on. Type talk any time to turn it off.")
|
|
304
|
+
else:
|
|
305
|
+
print("Staying quiet. Type talk any time to turn the voice on.")
|
|
160
306
|
return decision
|
|
161
307
|
|
|
308
|
+
|
|
162
309
|
class ChipVoiceSystem:
|
|
163
310
|
"""
|
|
164
311
|
Voice synthesis system for Chip O'Theseus
|
|
@@ -233,8 +380,9 @@ class ChipVoiceSystem:
|
|
|
233
380
|
def ensure_voice(self) -> bool:
|
|
234
381
|
"""Load the voice on first use, never at import. Returns readiness.
|
|
235
382
|
|
|
236
|
-
Reached
|
|
237
|
-
|
|
383
|
+
Reached after voice_consent() read yes, or from first_contact() after
|
|
384
|
+
its printed line, where the download comes before the question so the
|
|
385
|
+
question can be heard.
|
|
238
386
|
"""
|
|
239
387
|
if self.voice_ready:
|
|
240
388
|
return True
|
|
@@ -286,7 +434,7 @@ class ChipVoiceSystem:
|
|
|
286
434
|
|
|
287
435
|
|
|
288
436
|
|
|
289
|
-
def synthesize_and_play(self, text: str) -> bool:
|
|
437
|
+
def synthesize_and_play(self, text: str, ceremony: bool = False) -> bool:
|
|
290
438
|
"""
|
|
291
439
|
Synthesize text and play audio (Mike's tested approach)
|
|
292
440
|
|
|
@@ -296,9 +444,11 @@ class ChipVoiceSystem:
|
|
|
296
444
|
Returns:
|
|
297
445
|
bool: True if successful, False otherwise
|
|
298
446
|
"""
|
|
299
|
-
if voice_consent() != "yes":
|
|
447
|
+
if voice_consent() != "yes" and not ceremony:
|
|
300
448
|
# THE BARRIER. Every path to sound passes here, and the answer is
|
|
301
449
|
# read fresh each time, so a no recorded mid-session holds at once.
|
|
450
|
+
# ceremony=True has one caller, first_contact(), and one line: the
|
|
451
|
+
# question that asks whether the voice may speak, before any answer.
|
|
302
452
|
self.last_error = "voice not allowed"
|
|
303
453
|
logger.info("🎤 Voice not allowed; nothing spoken")
|
|
304
454
|
return False
|
|
@@ -695,16 +845,27 @@ def test_memory_voice_integration():
|
|
|
695
845
|
print("❌ No memories found to test")
|
|
696
846
|
return False
|
|
697
847
|
|
|
848
|
+
if __name__ == "__main__" and sys.argv[1:2] == ["talk"]:
|
|
849
|
+
# `talk`, the word the menu lists under walk: flip, or set with on/off.
|
|
850
|
+
raise SystemExit(talk_command(sys.argv[2:]))
|
|
851
|
+
if __name__ == "__main__" and sys.argv[1:2] == ["first"]:
|
|
852
|
+
# flake.nix's runScript calls this once per machine, right before the first
|
|
853
|
+
# menu; it exits 0 whatever happened, so the menu always prints after it.
|
|
854
|
+
try:
|
|
855
|
+
first_contact()
|
|
856
|
+
except Exception as _exc:
|
|
857
|
+
print(f"(the voice question was skipped: {type(_exc).__name__}: {_exc})")
|
|
858
|
+
raise SystemExit(0)
|
|
698
859
|
if __name__ == "__main__" and sys.argv[1:2] == ["ask"]:
|
|
699
|
-
# `voice
|
|
860
|
+
# `voice`: re-ask the whole card and record the answer, then speak one
|
|
700
861
|
# sentence on yes so the audio path is heard rather than assumed.
|
|
701
862
|
_env = os.environ.get("PIPULATE_VOICE", "").strip()
|
|
702
863
|
if _env:
|
|
703
864
|
print(f"voice: PIPULATE_VOICE={_env!r} decides; unset it to be asked.")
|
|
704
865
|
raise SystemExit(0)
|
|
705
|
-
_decision = ask_voice_consent(later_hint="
|
|
866
|
+
_decision = ask_voice_consent(later_hint="talk", force=True)
|
|
706
867
|
if _decision == "unavailable":
|
|
707
|
-
print("voice: no
|
|
868
|
+
print("voice: no keyboard to ask on; type voice at the command line.")
|
|
708
869
|
raise SystemExit(1)
|
|
709
870
|
print(f"voice: {_decision} (recorded in {VOICE_CONSENT_FILE})")
|
|
710
871
|
if _decision == "yes" and chip_voice_system is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipulate
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.74
|
|
4
4
|
Summary: The context compiler that helps you prompt well -- local-first, Nix-reproducible, replayable. The successor to AI SEO software.
|
|
5
5
|
Author-email: Mike Levin <pipulate@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -86,7 +86,7 @@ Under both sits `flake.nix` with its `flake.lock`: the dependency inventory an S
|
|
|
86
86
|
|
|
87
87
|
## Quick Start: Be Running in 5 Minutes
|
|
88
88
|
|
|
89
|
-
**Want to skip the philosophy and just see what this does?
|
|
89
|
+
**Want to skip the philosophy and just see what this does? Open the command line (on a Mac, the app called Terminal) and paste this:**
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
curl -fsSL https://pipulate.com/install.sh | bash
|
|
@@ -113,8 +113,8 @@ These few commands:
|
|
|
113
113
|
|
|
114
114
|
### 4. Run It Again!
|
|
115
115
|
|
|
116
|
-
1. You can just
|
|
117
|
-
2. Open a new
|
|
116
|
+
1. You can just close the window it's running in.
|
|
117
|
+
2. Open a new one, and once again:
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
120
|
cd ~/pipulate
|
|
@@ -242,7 +242,7 @@ Pipulate is a **local-first, single-tenant desktop app framework** featuring AI-
|
|
|
242
242
|
│ │
|
|
243
243
|
▼ ▼
|
|
244
244
|
┌─────────────────────────┐ ┌─────────────────────────┐ This is new.
|
|
245
|
-
│ 📱 Native Window │ │ 🖥️
|
|
245
|
+
│ 📱 Native Window │ │ 🖥️ The Command Line │ , O
|
|
246
246
|
│ ┌─────────────────┐ │ │ ┌─────────────────────┐ │ \\ . O
|
|
247
247
|
│ │ Web Browser │ │ │ │ nix develop (./run) │ │ |\\/| o
|
|
248
248
|
│ │ (Bundled) │ │ │ │ Starting servers... │ │ / " '\
|
|
@@ -628,7 +628,7 @@ Type `jn` at the `(nix)` prompt, wait for the JupyterLab tab to open, then run t
|
|
|
628
628
|
|
|
629
629
|
| Problem | Solution |
|
|
630
630
|
|---------|----------|
|
|
631
|
-
| `nix: command not found` |
|
|
631
|
+
| `nix: command not found` | Close the window and open a new one; Nix needs a fresh window after it installs |
|
|
632
632
|
| Browser doesn't open automatically | Manually visit `http://localhost:5001` and `http://localhost:8888` |
|
|
633
633
|
| `Permission denied` errors | Make sure you can write to `~/pipulate` directory |
|
|
634
634
|
| Port conflicts | Kill processes on ports 5001/8888: `lsof -ti:5001 \| xargs kill -9` |
|
|
@@ -656,7 +656,7 @@ If you are a developer comfortable with tools like Homebrew and `pipx`, you can
|
|
|
656
656
|
brew install pipx
|
|
657
657
|
```
|
|
658
658
|
|
|
659
|
-
**Step 2: Install the
|
|
659
|
+
**Step 2: Install the `pipulate` command**
|
|
660
660
|
|
|
661
661
|
Use `pipx` to install the `pipulate` command-line tool. This will not cause conflicts with your system Python.
|
|
662
662
|
|
|
@@ -871,7 +871,7 @@ Traditional development follows DRY principles, creating abstract, complex syste
|
|
|
871
871
|
|
|
872
872
|
## Developer Setup & Environment Notes
|
|
873
873
|
|
|
874
|
-
**Nix Environment Activation:** Always run `nix develop` from the `~/pipulate` directory *before* running any project commands (`python server.py`, `pip install`, etc.) in a new
|
|
874
|
+
**Nix Environment Activation:** Always run `nix develop` from the `~/pipulate` directory *before* running any project commands (`python server.py`, `pip install`, etc.) in a new window. This ensures you are using the correct dependencies defined in `flake.nix`.
|
|
875
875
|
|
|
876
876
|
**Interactive vs. Quiet Shell:**
|
|
877
877
|
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pipulate"
|
|
7
7
|
# VERSION NOTE: Synced from pipulate/__init__.py.__version__ via python version_sync.py
|
|
8
|
-
version = "2.
|
|
8
|
+
version = "2.74"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="Mike Levin", email="pipulate@gmail.com" },
|
|
11
11
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|