pi-superwhisper-paste 0.1.0
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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/docs/examples.md +42 -0
- package/docs/github-template.md +43 -0
- package/docs/release.md +38 -0
- package/docs/repository-settings.md +40 -0
- package/docs/template-checklist.md +85 -0
- package/docs/typescript.md +75 -0
- package/extensions/index.ts +301 -0
- package/package.json +62 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
This project follows semantic versioning.
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - YYYY-MM-DD
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Initial Superwhisper paste bridge extension for Pi on Windows.
|
|
12
|
+
- Default-on clipboard watcher with `/sw-paste:on` and `/sw-paste:off` controls.
|
|
13
|
+
- CI and npm Trusted Publishing workflow from the Pi extension template.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 YOUR_NAME
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Pi Superwhisper Paste
|
|
2
|
+
|
|
3
|
+
[](https://github.com/eiei114/pi-superwhisper-paste/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/eiei114/pi-superwhisper-paste/actions/workflows/publish.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/pi-superwhisper-paste)
|
|
6
|
+
[](https://www.npmjs.com/package/pi-superwhisper-paste)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://pi.dev/packages)
|
|
9
|
+
[](docs/release.md)
|
|
10
|
+
|
|
11
|
+
> Bridge Superwhisper clipboard dictation into the Pi TUI editor on Windows.
|
|
12
|
+
|
|
13
|
+
## Why This Exists
|
|
14
|
+
|
|
15
|
+
Superwhisper can paste dictated text into normal Windows apps, but terminal TUIs can be a rough edge. In the original case, dictation worked in Notepad and PowerShell, and Typeless worked in Pi, but Superwhisper's automatic paste did not reliably reach Pi's TUI editor without a manual `Ctrl+V`.
|
|
16
|
+
|
|
17
|
+
Pi Superwhisper Paste fixes that gap from the Pi side. It watches the Windows clipboard, detects new Superwhisper output, and inserts it into the active Pi editor with Pi's extension API.
|
|
18
|
+
|
|
19
|
+
## Observed Environment
|
|
20
|
+
|
|
21
|
+
This package was created for an issue observed in one local setup:
|
|
22
|
+
|
|
23
|
+
- Windows
|
|
24
|
+
- VS Code integrated terminal
|
|
25
|
+
- PowerShell
|
|
26
|
+
- Pi running in the terminal
|
|
27
|
+
- Superwhisper using clipboard-based auto-paste
|
|
28
|
+
|
|
29
|
+
This may not be a universal Superwhisper or Pi issue. If Superwhisper already pastes into your Pi editor reliably, you probably do not need this package.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Default-on after the extension loads.
|
|
34
|
+
- Inserts new clipboard text into the active Pi editor.
|
|
35
|
+
- Avoids replaying clipboard content that existed before startup.
|
|
36
|
+
- Pastes only into the active Pi tab when multiple tabs are open.
|
|
37
|
+
- Ignores old clipboard content when switching tabs.
|
|
38
|
+
- Continues inserting while Pi is thinking or waiting on an agent turn.
|
|
39
|
+
- Provides two control commands: `/sw-paste:on` and `/sw-paste:off`.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pi install npm:pi-superwhisper-paste
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or install from GitHub:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pi install git:github.com/eiei114/pi-superwhisper-paste
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
After installation, start Pi normally. The bridge is enabled by default.
|
|
56
|
+
|
|
57
|
+
Dictate with Superwhisper while the Pi input box is focused. When Superwhisper writes the transcript to the clipboard, the active Pi tab receives it automatically.
|
|
58
|
+
|
|
59
|
+
Toggle the bridge inside Pi's input box:
|
|
60
|
+
|
|
61
|
+
```txt
|
|
62
|
+
/sw-paste:off
|
|
63
|
+
/sw-paste:on
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The status line shows whether this tab is ready to receive dictation:
|
|
67
|
+
|
|
68
|
+
```txt
|
|
69
|
+
SW paste: active
|
|
70
|
+
SW paste: standby
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Local Development
|
|
74
|
+
|
|
75
|
+
Run this repo as a local Pi extension:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pi -e .
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then run:
|
|
82
|
+
|
|
83
|
+
```txt
|
|
84
|
+
/sw-paste:off
|
|
85
|
+
/sw-paste:on
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Behavior
|
|
89
|
+
|
|
90
|
+
### Multiple Pi tabs
|
|
91
|
+
|
|
92
|
+
Only the tab that most recently had terminal focus or keyboard input should paste the new clipboard text. Other tabs stay in standby.
|
|
93
|
+
|
|
94
|
+
When you switch to another Pi tab, the current clipboard is treated as already seen. This prevents the previous dictation result from being pasted into the newly focused tab.
|
|
95
|
+
|
|
96
|
+
### Pi thinking state
|
|
97
|
+
|
|
98
|
+
The bridge does not wait for Pi's agent turn to become idle. If Pi is thinking and the active tab is focused, the next Superwhisper clipboard update is still inserted into the editor.
|
|
99
|
+
|
|
100
|
+
### Clipboard baseline
|
|
101
|
+
|
|
102
|
+
On startup and focus changes, existing clipboard text is used as a baseline and will not be pasted. Only later clipboard changes are treated as dictation input.
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
| Variable | Default | Purpose |
|
|
107
|
+
|---|---:|---|
|
|
108
|
+
| `PI_SUPERWHISPER_PASTE` | `on` | Set to `off`, `0`, `false`, or `no` to disable by default. |
|
|
109
|
+
| `PI_SUPERWHISPER_PASTE_INTERVAL_MS` | `800` | Clipboard polling interval. |
|
|
110
|
+
| `PI_SUPERWHISPER_PASTE_MAX_CHARS` | `8000` | Maximum clipboard text length to paste. |
|
|
111
|
+
|
|
112
|
+
PowerShell example:
|
|
113
|
+
|
|
114
|
+
```powershell
|
|
115
|
+
$env:PI_SUPERWHISPER_PASTE = "off"
|
|
116
|
+
pi
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Troubleshooting
|
|
120
|
+
|
|
121
|
+
If dictation does not appear, first confirm that Superwhisper can paste into Notepad or PowerShell. If it cannot, fix Superwhisper's own auto-paste settings first.
|
|
122
|
+
|
|
123
|
+
If dictation appears in the wrong Pi tab, reload all open Pi tabs so old extension instances are gone.
|
|
124
|
+
|
|
125
|
+
If text appears only after pressing a key, reload Pi and confirm the status line shows `SW paste: active`.
|
|
126
|
+
|
|
127
|
+
## Package contents
|
|
128
|
+
|
|
129
|
+
| Path | Purpose |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `extensions/` | Pi TypeScript extension entrypoints (`*.ts` and `index.ts`) |
|
|
132
|
+
| `docs/` | Release and setup docs |
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm install
|
|
138
|
+
npm run ci
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`npm run ci` runs typecheck, tests, and `npm pack --dry-run`.
|
|
142
|
+
|
|
143
|
+
## Release
|
|
144
|
+
|
|
145
|
+
This package is set up for npm Trusted Publishing, so no `NPM_TOKEN` is required.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm version patch
|
|
149
|
+
git push --follow-tags
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
See [`docs/release.md`](docs/release.md) for setup details.
|
|
153
|
+
|
|
154
|
+
## Security
|
|
155
|
+
|
|
156
|
+
Pi packages can execute code with your local permissions. Review extensions before installing third-party packages.
|
|
157
|
+
|
|
158
|
+
For vulnerability reporting, see [`SECURITY.md`](SECURITY.md).
|
|
159
|
+
|
|
160
|
+
## Links
|
|
161
|
+
|
|
162
|
+
- npm: https://www.npmjs.com/package/pi-superwhisper-paste
|
|
163
|
+
- GitHub: https://github.com/eiei114/pi-superwhisper-paste
|
|
164
|
+
- Issues: https://github.com/eiei114/pi-superwhisper-paste/issues
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
package/docs/examples.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
This package ships one Pi extension for Windows clipboard-based dictation.
|
|
4
|
+
|
|
5
|
+
## Local trial
|
|
6
|
+
|
|
7
|
+
Run Pi with this repo loaded as an extension:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi -e .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The bridge is enabled by default. Dictate with Superwhisper and let it update the clipboard.
|
|
14
|
+
|
|
15
|
+
## Toggle commands
|
|
16
|
+
|
|
17
|
+
Run these commands inside Pi's input box:
|
|
18
|
+
|
|
19
|
+
```txt
|
|
20
|
+
/sw-paste:off
|
|
21
|
+
/sw-paste:on
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Environment variables
|
|
25
|
+
|
|
26
|
+
| Variable | Default | Purpose |
|
|
27
|
+
|---|---:|---|
|
|
28
|
+
| `PI_SUPERWHISPER_PASTE` | `on` | Set to `off`, `0`, `false`, or `no` to disable by default. |
|
|
29
|
+
| `PI_SUPERWHISPER_PASTE_INTERVAL_MS` | `800` | Clipboard polling interval. |
|
|
30
|
+
| `PI_SUPERWHISPER_PASTE_MAX_CHARS` | `8000` | Maximum clipboard text length to paste. |
|
|
31
|
+
|
|
32
|
+
## Expected workflow
|
|
33
|
+
|
|
34
|
+
1. Start Pi with this extension installed or loaded.
|
|
35
|
+
2. Keep the Pi input box focused.
|
|
36
|
+
3. Dictate with Superwhisper.
|
|
37
|
+
4. Superwhisper writes the transcript to the clipboard.
|
|
38
|
+
5. The extension inserts the new clipboard text into the Pi editor.
|
|
39
|
+
|
|
40
|
+
When multiple Pi tabs are open, only the tab that most recently had terminal focus or keyboard input should paste the new clipboard text.
|
|
41
|
+
|
|
42
|
+
The bridge does not wait for Pi's agent turn to become idle. If Pi is thinking, the active tab should still receive the next Superwhisper clipboard update.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# GitHub Template Repository
|
|
2
|
+
|
|
3
|
+
Enable template mode on the source repository:
|
|
4
|
+
|
|
5
|
+
```txt
|
|
6
|
+
GitHub repo → Settings → General → Template repository
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Create a public repository from the template:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
gh repo create OWNER/new-pi-extension \
|
|
13
|
+
--public \
|
|
14
|
+
--template OWNER/pi-extension-template \
|
|
15
|
+
--clone
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Create a private repository from the template:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
gh repo create OWNER/new-pi-extension \
|
|
22
|
+
--private \
|
|
23
|
+
--template OWNER/pi-extension-template \
|
|
24
|
+
--clone
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Include all branches if needed:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gh repo create OWNER/new-pi-extension \
|
|
31
|
+
--public \
|
|
32
|
+
--template OWNER/pi-extension-template \
|
|
33
|
+
--include-all-branches \
|
|
34
|
+
--clone
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
After creation:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd new-pi-extension
|
|
41
|
+
npm install
|
|
42
|
+
npm run ci
|
|
43
|
+
```
|
package/docs/release.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Release
|
|
2
|
+
|
|
3
|
+
This package uses npm Trusted Publishing with GitHub Actions OIDC.
|
|
4
|
+
|
|
5
|
+
Do not add `NPM_TOKEN` or long-lived npm tokens to GitHub Secrets.
|
|
6
|
+
|
|
7
|
+
## One-time npm setup
|
|
8
|
+
|
|
9
|
+
On npmjs.com, configure Trusted Publishing for this package:
|
|
10
|
+
|
|
11
|
+
- Publisher: GitHub Actions
|
|
12
|
+
- Repository: this GitHub repository
|
|
13
|
+
- Workflow filename: `publish.yml`
|
|
14
|
+
|
|
15
|
+
## Publish
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm version patch
|
|
19
|
+
git push --follow-tags
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The `v*` tag triggers `.github/workflows/publish.yml`, which runs CI and publishes to npm.
|
|
23
|
+
|
|
24
|
+
## GitHub Actions requirements
|
|
25
|
+
|
|
26
|
+
- `permissions: id-token: write`
|
|
27
|
+
- GitHub-hosted runner
|
|
28
|
+
- No `NPM_TOKEN`
|
|
29
|
+
- `npm publish` from the configured workflow file
|
|
30
|
+
|
|
31
|
+
## First release checklist
|
|
32
|
+
|
|
33
|
+
- [ ] `package.json` name is final
|
|
34
|
+
- [ ] `repository.url` points to the real GitHub repository
|
|
35
|
+
- [ ] npm Trusted Publisher is configured
|
|
36
|
+
- [ ] `npm run ci` passes
|
|
37
|
+
- [ ] `npm pack --dry-run` contains only intended files
|
|
38
|
+
- [ ] `CHANGELOG.md` has the release date
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Repository Settings
|
|
2
|
+
|
|
3
|
+
Use this file after creating a real repository from the template.
|
|
4
|
+
|
|
5
|
+
## GitHub About
|
|
6
|
+
|
|
7
|
+
Suggested fields:
|
|
8
|
+
|
|
9
|
+
- Description: Bridge Superwhisper clipboard dictation into the Pi TUI editor on Windows.
|
|
10
|
+
- Website: npm package URL or project docs URL
|
|
11
|
+
- Topics:
|
|
12
|
+
- `pi`
|
|
13
|
+
- `pi-package`
|
|
14
|
+
- `pi-extension`
|
|
15
|
+
- `superwhisper`
|
|
16
|
+
- `clipboard`
|
|
17
|
+
- `windows`
|
|
18
|
+
- `typescript`
|
|
19
|
+
|
|
20
|
+
## Template mode
|
|
21
|
+
|
|
22
|
+
This repository is a concrete package, not a template. Do not enable template mode unless it is intentionally turned into a reusable starter.
|
|
23
|
+
|
|
24
|
+
## Branch protection
|
|
25
|
+
|
|
26
|
+
Recommended for public packages:
|
|
27
|
+
|
|
28
|
+
- Require pull request before merging
|
|
29
|
+
- Require status checks to pass
|
|
30
|
+
- Require `CI` workflow
|
|
31
|
+
- Block force pushes on the default branch
|
|
32
|
+
|
|
33
|
+
## npm package page
|
|
34
|
+
|
|
35
|
+
After first publish:
|
|
36
|
+
|
|
37
|
+
- Confirm README renders correctly
|
|
38
|
+
- Confirm package provenance appears
|
|
39
|
+
- Confirm package contents are intentional
|
|
40
|
+
- Add npm URL to GitHub About and README
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Template Setup Checklist
|
|
2
|
+
|
|
3
|
+
このテンプレートから新しい Pi 拡張OSSを作った後に埋めること。
|
|
4
|
+
|
|
5
|
+
## Repository
|
|
6
|
+
|
|
7
|
+
- [ ] GitHub repository name を決める
|
|
8
|
+
- [ ] GitHub About 欄を書く
|
|
9
|
+
- [ ] GitHub topics を設定する
|
|
10
|
+
- [ ] `pi`
|
|
11
|
+
- [ ] `pi-package`
|
|
12
|
+
- [ ] `agent-skill`
|
|
13
|
+
- [ ] `typescript`
|
|
14
|
+
- [ ] GitHub Settingsで `Template repository` をONにする
|
|
15
|
+
- [ ] Repository URL を `package.json` に反映する
|
|
16
|
+
- [ ] README の `OWNER/REPO` を実リポジトリに置き換える
|
|
17
|
+
|
|
18
|
+
## Package metadata
|
|
19
|
+
|
|
20
|
+
- [ ] `package.json` の `name` を変更する
|
|
21
|
+
- [ ] `description` を書く
|
|
22
|
+
- [ ] `author` を入れる
|
|
23
|
+
- [ ] `repository.url` を埋める
|
|
24
|
+
- [ ] `bugs.url` を埋める
|
|
25
|
+
- [ ] `homepage` を埋める
|
|
26
|
+
- [ ] `keywords` を見直す
|
|
27
|
+
- [ ] `LICENSE` の年・名前を更新する
|
|
28
|
+
|
|
29
|
+
## README placeholders
|
|
30
|
+
|
|
31
|
+
- [ ] `PACKAGE_DISPLAY_NAME` を置き換える
|
|
32
|
+
- [ ] `PACKAGE_NAME` を置き換える
|
|
33
|
+
- [ ] `OWNER/REPO` を置き換える
|
|
34
|
+
- [ ] one-line pitch を書く
|
|
35
|
+
- [ ] feature list を書く
|
|
36
|
+
- [ ] quick start command を実コマンドにする
|
|
37
|
+
- [ ] npm URL を確認する
|
|
38
|
+
- [ ] GitHub URL を確認する
|
|
39
|
+
|
|
40
|
+
## Pi package manifest
|
|
41
|
+
|
|
42
|
+
- [ ] `pi.extensions` に公開する拡張だけを残す
|
|
43
|
+
- [ ] `pi.skills` に公開する skill だけを残す
|
|
44
|
+
- [ ] 不要なら `prompts/` を消す
|
|
45
|
+
- [ ] 不要なら `themes/` を消す
|
|
46
|
+
- [ ] サンプル名を実名に変える
|
|
47
|
+
|
|
48
|
+
## TypeScript
|
|
49
|
+
|
|
50
|
+
- [ ] `extensions/index.ts` を実装に合わせて更新する
|
|
51
|
+
- [ ] `extensions/hello.ts` が不要なら削除する
|
|
52
|
+
- [ ] 共通ロジックを `lib/` に切り出す
|
|
53
|
+
- [ ] `strict: true` を維持する
|
|
54
|
+
- [ ] custom tool parameters は TypeBox schema で定義する
|
|
55
|
+
- [ ] string choices は `StringEnum` helper を使う
|
|
56
|
+
- [ ] runtime dependency は `dependencies`、Pi提供packageは `peerDependencies` に置く
|
|
57
|
+
- [ ] `package.json.files` に公開対象だけを入れる
|
|
58
|
+
|
|
59
|
+
## GitHub Template repo
|
|
60
|
+
|
|
61
|
+
- [ ] `gh repo create --template OWNER/pi-extension-template` で作成できることを確認する
|
|
62
|
+
- [ ] public/privateどちらの作成例もdocsに載せる
|
|
63
|
+
|
|
64
|
+
## CI / Release
|
|
65
|
+
|
|
66
|
+
- [ ] `npm run ci` が通る
|
|
67
|
+
- [ ] `npm pack --dry-run` が通る
|
|
68
|
+
- [ ] npm Trusted Publishing を設定する
|
|
69
|
+
- [ ] `NPM_TOKEN` を使っていないことを確認する
|
|
70
|
+
- [ ] tag publish が動くことを初回リリースで確認する
|
|
71
|
+
|
|
72
|
+
## npm page
|
|
73
|
+
|
|
74
|
+
- [ ] npm package URL を README に追加する
|
|
75
|
+
- [ ] npm description が適切に表示されるか確認する
|
|
76
|
+
- [ ] provenance が付いているか確認する
|
|
77
|
+
- [ ] 不要なファイルが package に含まれていないか確認する
|
|
78
|
+
|
|
79
|
+
## Before first release
|
|
80
|
+
|
|
81
|
+
- [ ] サンプルコードを実機 Pi でロードする
|
|
82
|
+
- [ ] `pi install git:github.com/OWNER/REPO` を試す
|
|
83
|
+
- [ ] `pi -e .` を試す
|
|
84
|
+
- [ ] README のコマンドがコピペで動くか確認する
|
|
85
|
+
- [ ] CHANGELOG に `0.1.0` を書く
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# TypeScript Guide
|
|
2
|
+
|
|
3
|
+
This is a TypeScript-first Pi package template.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
```txt
|
|
8
|
+
extensions/*.ts Pi extension entrypoints
|
|
9
|
+
lib/*.ts Shared TypeScript helpers
|
|
10
|
+
skills/*/SKILL.md Agent Skills
|
|
11
|
+
prompts/*.md Prompt templates
|
|
12
|
+
themes/*.json Themes
|
|
13
|
+
tests/*.test.mjs Smoke tests
|
|
14
|
+
tests/*.test.ts Optional TypeScript tests if you add a TS test runner
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Pi loads TypeScript extensions directly, so no build step is required for normal use.
|
|
18
|
+
|
|
19
|
+
## Strict mode
|
|
20
|
+
|
|
21
|
+
`tsconfig.json` keeps `strict: true`. Prefer fixing types over loosening compiler options.
|
|
22
|
+
|
|
23
|
+
## Extension entrypoints
|
|
24
|
+
|
|
25
|
+
Two entrypoint styles are shown:
|
|
26
|
+
|
|
27
|
+
- `extensions/hello.ts`: single-file extension
|
|
28
|
+
- `extensions/index.ts`: index-style extension that imports shared code from `lib/`
|
|
29
|
+
|
|
30
|
+
For larger packages, keep entrypoints thin and put reusable logic in `lib/`.
|
|
31
|
+
|
|
32
|
+
## TypeBox schemas
|
|
33
|
+
|
|
34
|
+
Use TypeBox schemas for custom tool parameters.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { Type } from "typebox";
|
|
38
|
+
|
|
39
|
+
const parameters = Type.Object({
|
|
40
|
+
name: Type.String({ description: "Name to greet" }),
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## String enums
|
|
45
|
+
|
|
46
|
+
For string choices, use the local `StringEnum` helper from `lib/schema.ts`.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { StringEnum } from "../lib/schema.ts";
|
|
50
|
+
|
|
51
|
+
const mode = StringEnum(["short", "friendly"], {
|
|
52
|
+
description: "Greeting style",
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This emits a JSON Schema `enum`, which is friendlier to model providers than a union of string literals.
|
|
57
|
+
|
|
58
|
+
## Runtime dependencies vs peer dependencies
|
|
59
|
+
|
|
60
|
+
Pi bundles core packages for extension authors. Keep Pi-provided packages as `peerDependencies` and also install them as `devDependencies` for local typechecking.
|
|
61
|
+
|
|
62
|
+
Use `peerDependencies` for:
|
|
63
|
+
|
|
64
|
+
- `@earendil-works/pi-coding-agent`
|
|
65
|
+
- `@earendil-works/pi-ai`
|
|
66
|
+
- `@earendil-works/pi-tui`
|
|
67
|
+
- `typebox`
|
|
68
|
+
|
|
69
|
+
Use `dependencies` for runtime packages your extension imports that Pi does not provide.
|
|
70
|
+
|
|
71
|
+
Use `devDependencies` for local-only tools such as TypeScript, test runners, and linters.
|
|
72
|
+
|
|
73
|
+
## Package contents
|
|
74
|
+
|
|
75
|
+
Control npm package contents with `package.json` `files`. Prefer this over `.npmignore` so the published package stays explicit.
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
const STATUS_KEY = "superwhisper-paste";
|
|
11
|
+
const DEFAULT_INTERVAL_MS = 800;
|
|
12
|
+
const DEFAULT_MAX_CHARS = 8000;
|
|
13
|
+
const ACTIVE_STATE_FILE = "pi-superwhisper-paste-active.json";
|
|
14
|
+
const FOCUS_IN = "\x1b[I";
|
|
15
|
+
const FOCUS_OUT = "\x1b[O";
|
|
16
|
+
const ENABLE_FOCUS_REPORTING = "\x1b[?1004h";
|
|
17
|
+
const DISABLE_FOCUS_REPORTING = "\x1b[?1004l";
|
|
18
|
+
const INSTANCE_ID = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
19
|
+
|
|
20
|
+
type BridgeMode = "off" | "on";
|
|
21
|
+
|
|
22
|
+
type PiEditorUi = {
|
|
23
|
+
setStatus(key: string, value: string | undefined): void;
|
|
24
|
+
notify(message: string, level?: "info" | "warning" | "error" | "success"): void;
|
|
25
|
+
onTerminalInput(
|
|
26
|
+
handler: (data: string) => { consume?: boolean; data?: string } | undefined,
|
|
27
|
+
): () => void;
|
|
28
|
+
pasteToEditor(text: string): void;
|
|
29
|
+
getEditorText?: () => string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type PiRuntimeContext = {
|
|
33
|
+
hasUI?: boolean;
|
|
34
|
+
ui: PiEditorUi;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type BridgeState = {
|
|
38
|
+
mode: BridgeMode;
|
|
39
|
+
interval?: NodeJS.Timeout;
|
|
40
|
+
inFlight: boolean;
|
|
41
|
+
activating: boolean;
|
|
42
|
+
terminalFocused: boolean;
|
|
43
|
+
unsubscribeTerminalInput?: () => void;
|
|
44
|
+
lastClipboard?: string;
|
|
45
|
+
lastPasted?: string;
|
|
46
|
+
lastPasteSummary?: string;
|
|
47
|
+
ctx?: PiRuntimeContext;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const state: BridgeState = {
|
|
51
|
+
mode: defaultMode(),
|
|
52
|
+
inFlight: false,
|
|
53
|
+
activating: false,
|
|
54
|
+
terminalFocused: true,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function defaultMode(): BridgeMode {
|
|
58
|
+
const raw = (process.env.PI_SUPERWHISPER_PASTE ?? "on").trim().toLowerCase();
|
|
59
|
+
return ["0", "false", "no", "off"].includes(raw) ? "off" : "on";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function intervalMs(): number {
|
|
63
|
+
const parsed = Number(process.env.PI_SUPERWHISPER_PASTE_INTERVAL_MS);
|
|
64
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_INTERVAL_MS;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function maxChars(): number {
|
|
68
|
+
const parsed = Number(process.env.PI_SUPERWHISPER_PASTE_MAX_CHARS);
|
|
69
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CHARS;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function statusText(): string | undefined {
|
|
73
|
+
if (state.mode === "off") return undefined;
|
|
74
|
+
const focusText = state.terminalFocused ? "active" : "standby";
|
|
75
|
+
return state.lastPasteSummary
|
|
76
|
+
? `SW paste: ${focusText} (${state.lastPasteSummary})`
|
|
77
|
+
: `SW paste: ${focusText}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function setStatus(ctx: PiRuntimeContext): void {
|
|
81
|
+
ctx.ui.setStatus(STATUS_KEY, statusText());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function markPasted(ctx: PiRuntimeContext, text: string): void {
|
|
85
|
+
state.lastPasted = text;
|
|
86
|
+
state.lastPasteSummary = `${text.length} chars`;
|
|
87
|
+
// pasteToEditor mutates the editor directly; setStatus nudges Pi to redraw.
|
|
88
|
+
setStatus(ctx);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readClipboardText(): Promise<string | undefined> {
|
|
92
|
+
const script = [
|
|
93
|
+
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
94
|
+
"$text = Get-Clipboard -Raw -Format Text -ErrorAction SilentlyContinue",
|
|
95
|
+
"if ($null -ne $text) { [Console]::Out.Write($text) }",
|
|
96
|
+
].join("; ");
|
|
97
|
+
|
|
98
|
+
const result = await execFileAsync(
|
|
99
|
+
"powershell.exe",
|
|
100
|
+
["-NoProfile", "-Command", script],
|
|
101
|
+
{
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
maxBuffer: maxChars() * 4,
|
|
104
|
+
timeout: 2500,
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return typeof result.stdout === "string" ? result.stdout : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function activeStatePath(): string {
|
|
113
|
+
return join(tmpdir(), ACTIVE_STATE_FILE);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function claimActiveTab(): Promise<void> {
|
|
117
|
+
const claim = {
|
|
118
|
+
instanceId: INSTANCE_ID,
|
|
119
|
+
pid: process.pid,
|
|
120
|
+
updatedAt: Date.now(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await writeFile(activeStatePath(), JSON.stringify(claim), "utf8");
|
|
125
|
+
} catch {
|
|
126
|
+
// Focus tracking is best-effort; clipboard safety still falls back to local focus state.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function isActiveTab(): Promise<boolean> {
|
|
131
|
+
if (!state.terminalFocused) return false;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const raw = await readFile(activeStatePath(), "utf8");
|
|
135
|
+
const claim = JSON.parse(raw) as { instanceId?: string };
|
|
136
|
+
return claim.instanceId === INSTANCE_ID;
|
|
137
|
+
} catch {
|
|
138
|
+
await claimActiveTab();
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function refreshClipboardBaseline(): Promise<void> {
|
|
144
|
+
const text = await readClipboardText();
|
|
145
|
+
if (text !== undefined) state.lastClipboard = text;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function activateTab(ctx: PiRuntimeContext): Promise<void> {
|
|
149
|
+
if (state.activating) return;
|
|
150
|
+
|
|
151
|
+
state.activating = true;
|
|
152
|
+
try {
|
|
153
|
+
state.terminalFocused = true;
|
|
154
|
+
await refreshClipboardBaseline();
|
|
155
|
+
await claimActiveTab();
|
|
156
|
+
setStatus(ctx);
|
|
157
|
+
} finally {
|
|
158
|
+
state.activating = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function markActiveFromInput(ctx: PiRuntimeContext): Promise<void> {
|
|
163
|
+
state.terminalFocused = true;
|
|
164
|
+
await claimActiveTab();
|
|
165
|
+
setStatus(ctx);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shouldPaste(text: string, ctx: PiRuntimeContext): boolean {
|
|
169
|
+
if (!text.trim()) return false;
|
|
170
|
+
if (text.length > maxChars()) return false;
|
|
171
|
+
if (text === state.lastPasted) return false;
|
|
172
|
+
|
|
173
|
+
const current = String(ctx.ui.getEditorText?.() ?? "");
|
|
174
|
+
if (current.endsWith(text)) return false;
|
|
175
|
+
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function pasteClipboardChange(ctx: PiRuntimeContext): Promise<void> {
|
|
180
|
+
if (state.inFlight || state.activating || state.mode === "off") return;
|
|
181
|
+
if (!ctx.hasUI) return;
|
|
182
|
+
if (!(await isActiveTab())) return;
|
|
183
|
+
|
|
184
|
+
state.inFlight = true;
|
|
185
|
+
try {
|
|
186
|
+
const text = await readClipboardText();
|
|
187
|
+
if (text === undefined || text === state.lastClipboard) return;
|
|
188
|
+
|
|
189
|
+
state.lastClipboard = text;
|
|
190
|
+
if (!shouldPaste(text, ctx)) return;
|
|
191
|
+
if (!(await isActiveTab())) return;
|
|
192
|
+
|
|
193
|
+
ctx.ui.pasteToEditor(text);
|
|
194
|
+
markPasted(ctx, text);
|
|
195
|
+
} catch {
|
|
196
|
+
// Clipboard polling should stay quiet while the user is typing.
|
|
197
|
+
} finally {
|
|
198
|
+
state.inFlight = false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function setupTerminalFocusTracking(ctx: PiRuntimeContext): void {
|
|
203
|
+
if (state.unsubscribeTerminalInput) return;
|
|
204
|
+
|
|
205
|
+
process.stdout.write(ENABLE_FOCUS_REPORTING);
|
|
206
|
+
void activateTab(ctx);
|
|
207
|
+
|
|
208
|
+
state.unsubscribeTerminalInput = ctx.ui.onTerminalInput((data) => {
|
|
209
|
+
if (data === FOCUS_IN) {
|
|
210
|
+
void activateTab(ctx);
|
|
211
|
+
return { consume: true };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (data === FOCUS_OUT) {
|
|
215
|
+
state.terminalFocused = false;
|
|
216
|
+
setStatus(ctx);
|
|
217
|
+
return { consume: true };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (data.length > 0) {
|
|
221
|
+
void markActiveFromInput(ctx);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return undefined;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function teardownTerminalFocusTracking(): void {
|
|
229
|
+
state.unsubscribeTerminalInput?.();
|
|
230
|
+
state.unsubscribeTerminalInput = undefined;
|
|
231
|
+
process.stdout.write(DISABLE_FOCUS_REPORTING);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function ensurePolling(ctx: PiRuntimeContext): void {
|
|
235
|
+
state.ctx = ctx;
|
|
236
|
+
setupTerminalFocusTracking(ctx);
|
|
237
|
+
setStatus(ctx);
|
|
238
|
+
|
|
239
|
+
if (state.interval) return;
|
|
240
|
+
state.interval = setInterval(() => {
|
|
241
|
+
if (state.ctx) void pasteClipboardChange(state.ctx);
|
|
242
|
+
}, intervalMs());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function stopPolling(ctx?: PiRuntimeContext): void {
|
|
246
|
+
if (state.interval) {
|
|
247
|
+
clearInterval(state.interval);
|
|
248
|
+
state.interval = undefined;
|
|
249
|
+
}
|
|
250
|
+
teardownTerminalFocusTracking();
|
|
251
|
+
state.mode = "off";
|
|
252
|
+
if (ctx) setStatus(ctx);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function startPolling(ctx: PiRuntimeContext): Promise<void> {
|
|
256
|
+
state.lastClipboard = await readClipboardText();
|
|
257
|
+
ensurePolling(ctx);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function arm(ctx: PiRuntimeContext): Promise<void> {
|
|
261
|
+
state.lastClipboard = await readClipboardText();
|
|
262
|
+
state.mode = "on";
|
|
263
|
+
ensurePolling(ctx);
|
|
264
|
+
ctx.ui.notify("Superwhisper paste bridge: enabled", "info");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parseControlCommand(text: string): BridgeMode | undefined {
|
|
268
|
+
const match = text.trim().match(/^\/sw-paste:(on|off)$/i);
|
|
269
|
+
if (!match) return undefined;
|
|
270
|
+
return match[1].toLowerCase() as BridgeMode;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function runControlAction(action: BridgeMode, ctx: PiRuntimeContext): Promise<void> {
|
|
274
|
+
if (action === "on") {
|
|
275
|
+
await arm(ctx);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
stopPolling(ctx);
|
|
280
|
+
ctx.ui.notify("Superwhisper paste bridge: disabled", "info");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export default function superwhisperPaste(pi: ExtensionAPI) {
|
|
284
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
285
|
+
const runtimeCtx = ctx as PiRuntimeContext;
|
|
286
|
+
state.ctx = runtimeCtx;
|
|
287
|
+
if (state.mode !== "off") await startPolling(runtimeCtx);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
pi.on("session_shutdown", () => {
|
|
291
|
+
stopPolling();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
pi.on("input", async (event, ctx) => {
|
|
295
|
+
const action = parseControlCommand(event.text);
|
|
296
|
+
if (!action) return { action: "continue" };
|
|
297
|
+
|
|
298
|
+
await runControlAction(action, ctx as PiRuntimeContext);
|
|
299
|
+
return { action: "handled" };
|
|
300
|
+
});
|
|
301
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-superwhisper-paste",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension that bridges Superwhisper clipboard output into the Pi TUI editor on Windows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Keisu",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi",
|
|
10
|
+
"pi-package",
|
|
11
|
+
"pi-extension",
|
|
12
|
+
"superwhisper",
|
|
13
|
+
"clipboard",
|
|
14
|
+
"windows",
|
|
15
|
+
"typescript"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/eiei114/pi-superwhisper-paste.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/eiei114/pi-superwhisper-paste/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/eiei114/pi-superwhisper-paste#readme",
|
|
25
|
+
"files": [
|
|
26
|
+
"extensions/",
|
|
27
|
+
"lib/",
|
|
28
|
+
"skills/",
|
|
29
|
+
"prompts/",
|
|
30
|
+
"themes/",
|
|
31
|
+
"docs/",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"CHANGELOG.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "node --test tests/*.test.mjs",
|
|
39
|
+
"ci": "npm run typecheck && npm test && npm run pack:check",
|
|
40
|
+
"pack:check": "npm pack --dry-run"
|
|
41
|
+
},
|
|
42
|
+
"pi": {
|
|
43
|
+
"extensions": [
|
|
44
|
+
"./extensions"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@earendil-works/pi-ai": "*",
|
|
52
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
53
|
+
"@earendil-works/pi-tui": "*"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@earendil-works/pi-ai": "latest",
|
|
57
|
+
"@earendil-works/pi-coding-agent": "latest",
|
|
58
|
+
"@earendil-works/pi-tui": "latest",
|
|
59
|
+
"@types/node": "^22.0.0",
|
|
60
|
+
"typescript": "^6.0.3"
|
|
61
|
+
}
|
|
62
|
+
}
|