@henryqw/pi-bark 0.1.2 → 0.2.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@henryqw/pi-bark`
2
2
 
3
- Use Bark as an out-of-band status channel for Pi sessions. The extension reports when Pi finishes or waits for input. `/copyb` can also send the last agent message.
3
+ Get [Bark](https://bark.day.app) notifications when Pi finishes or needs your input. Use `/copyb` to send Pi's latest response to your device.
4
4
 
5
5
  ## Install
6
6
 
@@ -51,8 +51,8 @@ The `body` value is the same text that Pi's `/copy` command selects. Markdown, c
51
51
  | --- | --- | --- |
52
52
  | `/copyb` | command | Copy the last agent message and send a Bark push notification. |
53
53
  | `/set-bark <device-key> [server-url]` | command | Save the Device Key and Bark server URL. |
54
- | `/bark-notifications on\|off\|inherit` | command | Override automatic status notifications for the current CWD. |
55
- | `/bark-notifications default on\|off` | command | Set the default for CWDs without an override. |
54
+ | `/bark on\|off\|inherit` | command | Override automatic status notifications for the current CWD. |
55
+ | `/bark default on\|off` | command | Set the default for CWDs without an override. |
56
56
  | `pi-bark-key [--force \| --disable]` | executable | Generate, replace, or disable the Custom Encryption Key. |
57
57
 
58
58
  ### Automatic status notifications
@@ -69,7 +69,7 @@ An unset session name appears as `Unnamed`. Status notifications do not include
69
69
  Automatic notifications are enabled by default. Disable them only in the current CWD:
70
70
 
71
71
  ```text
72
- /bark-notifications off
72
+ /bark off
73
73
  ```
74
74
 
75
75
  Use `on` to enable the current CWD explicitly. Use `inherit` to remove its override. The CWD then follows the global default.
@@ -77,22 +77,14 @@ Use `on` to enable the current CWD explicitly. Use `inherit` to remove its overr
77
77
  Change that default for all CWDs without an override:
78
78
 
79
79
  ```text
80
- /bark-notifications default off
80
+ /bark default off
81
81
  ```
82
82
 
83
83
  These settings do not affect `/copyb`. CWD overrides live in pi-bark's global config, not in project files.
84
84
 
85
85
  ## Flow
86
86
 
87
- ```mermaid
88
- flowchart LR
89
- copy["/copyb"] --> clipboard["Clipboard"]
90
- copy --> bark["pi-bark"]
91
- status["Pi status event"] --> bark
92
- bark --> request["Bark V2 /push request"]
93
- request --> server["Bark server"]
94
- server --> app["Bark App"]
95
- ```
87
+ ![Architecture showing copyb and Pi status notifications entering one shared sender, using AES256-GCM, passing ciphertext through the Bark server, and decrypting locally in the Bark App.](./docs/push-encryption-flow.svg)
96
88
 
97
89
  ## Config
98
90
 
@@ -107,39 +99,55 @@ Package-owned: `~/.pi/agent/config/pi-bark/config.json`
107
99
 
108
100
  ### Push encryption
109
101
 
110
- Push encryption is optional. It prevents the Bark server and Apple Push Notification service from reading the push content.
102
+ Push encryption is optional. It prevents the Bark server and Apple Push Notification service from reading push content.
111
103
 
112
- Generate and store a Custom Encryption Key:
104
+ Once configured, pi-bark encrypts every push it sends. This includes `/copyb` and both automatic status notifications.
113
105
 
114
- ```bash
115
- npx @henryqw/pi-bark
116
- ```
106
+ #### Set up encryption
107
+
108
+ 1. Configure your Bark Device Key with `/set-bark`.
109
+ 2. Generate and save a Custom Encryption Key:
110
+
111
+ ```bash
112
+ npx --yes --package=@henryqw/pi-bark -- pi-bark-key
113
+ ```
117
114
 
118
- The script prints the generated key once. Enter these exact settings in **Bark App Push Encryption**:
115
+ `npx` temporarily downloads the package and runs its `pi-bark-key` executable. The command saves the key to pi-bark's config and prints it once.
119
116
 
120
- | Setting | Value |
121
- | --- | --- |
122
- | Algorithm | `AES256` |
123
- | Mode | `GCM` |
124
- | Padding | `noPadding` |
125
- | Key | The generated 32-character key |
117
+ 3. Open **Bark App → Push Encryption**.
118
+ 4. Enter these settings:
126
119
 
127
- Do not paste script output into shared logs. The Custom Encryption Key can decrypt your push content.
120
+ | Setting | Value |
121
+ | --- | --- |
122
+ | Algorithm | `AES256` |
123
+ | Mode | `GCM` |
124
+ | Padding | `noPadding` |
125
+ | Key | The generated 32-character key |
128
126
 
129
- When enabled, pi-bark encrypts the JSON push content with a fresh 12-character IV. This includes status titles and session names. It sends `device_key`, `ciphertext`, and `iv` to the Bark server.
127
+ The generated ASCII key is 32 characters and 32 bytes.
128
+
129
+ 5. If Bark requires an IV, enter any 12 ASCII characters, such as `000000000000`.
130
+ 6. Save the Bark settings.
131
+ 7. Run `/copyb` in Pi. Bark should show the exact last agent message instead of `Decryption Failed`.
132
+
133
+ The saved IV is only a placeholder for older Bark versions. pi-bark creates a fresh 12-byte IV for each push and sends it with the ciphertext. Bark uses that per-push IV instead of the saved value.
134
+
135
+ Do not paste the command output into shared logs. The Custom Encryption Key can decrypt your push content.
130
136
 
131
137
  The GCM authentication tag is appended to the ciphertext before Base64 encoding, as Bark requires.
132
138
 
133
- The script protects an existing Custom Encryption Key. Replace it only when you intend to update the Bark App too:
139
+ #### Replace or disable the key
140
+
141
+ The command refuses to overwrite an existing key. Replace it only when you also update the Bark App:
134
142
 
135
143
  ```bash
136
- npx @henryqw/pi-bark --force
144
+ npx --yes --package=@henryqw/pi-bark -- pi-bark-key --force
137
145
  ```
138
146
 
139
147
  Disable push encryption in pi-bark with:
140
148
 
141
149
  ```bash
142
- npx @henryqw/pi-bark --disable
150
+ npx --yes --package=@henryqw/pi-bark -- pi-bark-key --disable
143
151
  ```
144
152
 
145
153
  Disable Push Encryption in the Bark App too. Otherwise its settings no longer match pi-bark.
@@ -1,9 +1,8 @@
1
1
  import { createCipheriv, randomBytes } from "node:crypto";
2
2
  import { ENCRYPTION_KEY_BYTES } from "./config.js";
3
- const RANDOM_TEXT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
4
3
  const GCM_IV_BYTES = 12;
5
4
  function randomText(length) {
6
- return Array.from(randomBytes(length), (byte) => RANDOM_TEXT_ALPHABET[byte & 63]).join("");
5
+ return randomBytes(length).toString("base64url").slice(0, length);
7
6
  }
8
7
  export function generateEncryptionKey() {
9
8
  return randomText(ENCRYPTION_KEY_BYTES);
@@ -0,0 +1,166 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>How pi-bark encrypts every push</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet">
8
+ <style>
9
+ *, *::before, *::after { box-sizing: border-box; }
10
+ html, body { margin: 0; min-height: 100%; }
11
+ body { display: grid; place-items: center; background: #f0eee9; }
12
+ figure { margin: 0; width: min(100%, 1280px); }
13
+ svg { display: block; width: 100%; height: auto; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <figure>
18
+ <svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="push-encryption-flow-title push-encryption-flow-desc">
19
+ <title id="push-encryption-flow-title">How pi-bark encrypts every push</title>
20
+ <desc id="push-encryption-flow-desc">Architecture diagram showing copyb messages and automatic Pi status events entering one shared sender. When the config contains a custom key, every push uses AES256-GCM with a fresh 12-byte IV before Bark forwards the ciphertext to the app for local decryption.</desc>
21
+ <defs>
22
+ <style>
23
+ .eyebrow { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
24
+ .heading { fill: #101828; font: 400 28px 'Instrument Serif', serif; }
25
+ .intro { fill: #4c5665; font: 400 12px 'Geist', sans-serif; }
26
+ .zone { fill: #e6e4de; stroke: #101828; stroke-opacity: .14; stroke-width: 1; }
27
+ .zone-label { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
28
+ .boundary { fill: none; stroke: #1d4ed8; stroke-opacity: .50; stroke-width: 1; stroke-dasharray: 4 4; }
29
+ .boundary-label { fill: #1d4ed8; font: 500 8px 'Geist Mono', monospace; letter-spacing: .12em; }
30
+ .node-mask { fill: #f0eee9; }
31
+ .node { fill: #ffffff; stroke: #101828; stroke-width: 1; }
32
+ .node-input { fill: #4c5665; fill-opacity: .10; stroke: #6a7282; stroke-width: 1; }
33
+ .node-store { fill: #101828; fill-opacity: .05; stroke: #4c5665; stroke-width: 1; }
34
+ .node-external { fill: #101828; fill-opacity: .03; stroke: #101828; stroke-opacity: .30; stroke-width: 1; }
35
+ .node-focal { fill: #1d4ed8; fill-opacity: .08; stroke: #1d4ed8; stroke-width: 1.2; }
36
+ .tag { fill: none; stroke: #101828; stroke-opacity: .34; stroke-width: .8; }
37
+ .tag-focal { fill: none; stroke: #1d4ed8; stroke-opacity: .55; stroke-width: .8; }
38
+ .tag-text { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .08em; text-anchor: middle; }
39
+ .tag-text-focal { fill: #1d4ed8; font: 500 8px 'Geist Mono', monospace; letter-spacing: .08em; text-anchor: middle; }
40
+ .name { fill: #101828; font: 600 12px 'Geist', sans-serif; text-anchor: middle; }
41
+ .sub { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; text-anchor: middle; }
42
+ .sub-focal { fill: #1d4ed8; font: 500 8px 'Geist Mono', monospace; text-anchor: middle; }
43
+ .edge { fill: none; stroke: #4c5665; stroke-width: 1.2; marker-end: url(#push-encryption-arrow); }
44
+ .edge-secure { fill: none; stroke: #1d4ed8; stroke-width: 1.2; marker-end: url(#push-encryption-arrow-accent); }
45
+ .label-mask { fill: #f0eee9; }
46
+ .label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; letter-spacing: .04em; text-anchor: middle; }
47
+ .label-secure { fill: #1d4ed8; font: 500 8px 'Geist Mono', monospace; letter-spacing: .04em; text-anchor: middle; }
48
+ .legend-title { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
49
+ .legend-text { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; }
50
+ </style>
51
+ <marker id="push-encryption-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
52
+ <polygon points="0 0,8 3,0 6" fill="#4c5665"/>
53
+ </marker>
54
+ <marker id="push-encryption-arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
55
+ <polygon points="0 0,8 3,0 6" fill="#1d4ed8"/>
56
+ </marker>
57
+ <marker id="push-encryption-arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
58
+ <polygon points="0 0,8 3,0 6" fill="#1d4ed8"/>
59
+ </marker>
60
+ </defs>
61
+
62
+ <rect width="1280" height="720" fill="#f0eee9"/>
63
+
64
+ <text class="eyebrow" x="40" y="48">ARCHITECTURE · PI-BARK</text>
65
+ <text class="heading" x="40" y="84">One encryption path for every Bark push</text>
66
+ <text class="intro" x="40" y="112">A configured key applies to manual output and automatic status notifications.</text>
67
+
68
+ <!-- Local trust zone and encrypted transport boundary -->
69
+ <rect class="zone" x="40" y="152" width="816" height="416" rx="8"/>
70
+ <rect class="label-mask" x="52" y="156" width="80" height="16" rx="2"/>
71
+ <text class="zone-label" x="60" y="168">LOCAL PI</text>
72
+
73
+ <line class="boundary" x1="880" y1="176" x2="880" y2="544"/>
74
+ <rect class="label-mask" x="892" y="180" width="240" height="16" rx="2"/>
75
+ <text class="boundary-label" x="900" y="192">CIPHERTEXT LEAVES PI</text>
76
+
77
+ <!-- Connectors are drawn before nodes -->
78
+ <path class="edge" d="M252 248 H292 Q300 248 300 256 V308 Q300 316 308 316 H352"/>
79
+ <path class="edge" d="M252 400 H304 Q312 400 312 392 V372 Q312 364 320 364 H352"/>
80
+ <line class="edge" x1="452" y1="452" x2="452" y2="396"/>
81
+ <line class="edge-secure" x1="552" y1="340" x2="648" y2="340"/>
82
+ <line class="edge-secure" x1="848" y1="340" x2="920" y2="340"/>
83
+ <line class="edge-secure" x1="1060" y1="340" x2="1100" y2="340"/>
84
+
85
+ <!-- Connector labels -->
86
+ <rect class="label-mask" x="464" y="416" width="72" height="12" rx="2"/>
87
+ <text class="label" x="500" y="424">LOAD EACH PUSH</text>
88
+
89
+ <rect class="label-mask" x="568" y="320" width="64" height="12" rx="2"/>
90
+ <text class="label-secure" x="600" y="328">KEY PRESENT</text>
91
+
92
+ <rect class="label-mask" x="856" y="320" width="56" height="12" rx="2"/>
93
+ <text class="label-secure" x="884" y="328">POST /PUSH</text>
94
+
95
+ <!-- Opaque node masks -->
96
+ <rect class="node-mask" x="72" y="208" width="180" height="80" rx="6"/>
97
+ <rect class="node-mask" x="72" y="360" width="180" height="80" rx="6"/>
98
+ <rect class="node-mask" x="352" y="284" width="200" height="112" rx="6"/>
99
+ <rect class="node-mask" x="352" y="452" width="200" height="80" rx="6"/>
100
+ <rect class="node-mask" x="648" y="284" width="200" height="112" rx="6"/>
101
+ <rect class="node-mask" x="920" y="284" width="140" height="112" rx="6"/>
102
+ <rect class="node-mask" x="1100" y="284" width="140" height="112" rx="6"/>
103
+
104
+ <!-- Inputs -->
105
+ <rect class="node-input" x="72" y="208" width="180" height="80" rx="6"/>
106
+ <rect class="tag" x="84" y="220" width="56" height="16" rx="2"/>
107
+ <text class="tag-text" x="112" y="232">COMMAND</text>
108
+ <text class="name" x="162" y="256">/copyb</text>
109
+ <text class="sub" x="162" y="276">LAST AGENT MESSAGE</text>
110
+
111
+ <rect class="node-input" x="72" y="360" width="180" height="80" rx="6"/>
112
+ <rect class="tag" x="84" y="372" width="48" height="16" rx="2"/>
113
+ <text class="tag-text" x="108" y="384">EVENT</text>
114
+ <text class="name" x="162" y="408">Pi status</text>
115
+ <text class="sub" x="162" y="428">NEEDS INPUT · FINISHED</text>
116
+
117
+ <!-- Shared sender and config -->
118
+ <rect class="node" x="352" y="284" width="200" height="112" rx="6"/>
119
+ <rect class="tag" x="364" y="296" width="48" height="16" rx="2"/>
120
+ <text class="tag-text" x="388" y="308">SHARED</text>
121
+ <text class="name" x="452" y="340">Push sender</text>
122
+ <text class="sub" x="452" y="364">ONE PATH FOR EVERY PUSH</text>
123
+ <text class="sub" x="452" y="380">DEVICE KEY + CONTENT</text>
124
+
125
+ <rect class="node-store" x="352" y="452" width="200" height="80" rx="6"/>
126
+ <rect class="tag" x="364" y="464" width="48" height="16" rx="2"/>
127
+ <text class="tag-text" x="388" y="476">CONFIG</text>
128
+ <text class="name" x="452" y="500">pi-bark config</text>
129
+ <text class="sub" x="452" y="520">DEVICE KEY · CUSTOM KEY</text>
130
+
131
+ <!-- Encryption focal point -->
132
+ <rect class="node-focal" x="648" y="284" width="200" height="112" rx="6"/>
133
+ <rect class="tag-focal" x="660" y="296" width="56" height="16" rx="2"/>
134
+ <text class="tag-text-focal" x="688" y="308">ENCRYPT</text>
135
+ <text class="name" x="748" y="340">AES256-GCM</text>
136
+ <text class="sub-focal" x="748" y="364">FRESH 12-BYTE IV</text>
137
+ <text class="sub-focal" x="748" y="380">AUTH TAG APPENDED</text>
138
+
139
+ <!-- External delivery -->
140
+ <rect class="node-external" x="920" y="284" width="140" height="112" rx="6"/>
141
+ <rect class="tag" x="932" y="296" width="48" height="16" rx="2"/>
142
+ <text class="tag-text" x="956" y="308">SERVER</text>
143
+ <text class="name" x="990" y="340">Bark server</text>
144
+ <text class="sub" x="990" y="364">FORWARDS ONLY</text>
145
+ <text class="sub" x="990" y="380">CANNOT READ CONTENT</text>
146
+
147
+ <rect class="node" x="1100" y="284" width="140" height="112" rx="6"/>
148
+ <rect class="tag" x="1112" y="296" width="32" height="16" rx="2"/>
149
+ <text class="tag-text" x="1128" y="308">APP</text>
150
+ <text class="name" x="1170" y="340">Bark App</text>
151
+ <text class="sub" x="1170" y="364">MATCHING 32-BYTE KEY</text>
152
+ <text class="sub" x="1170" y="380">DECRYPTS LOCALLY</text>
153
+
154
+ <!-- Legend -->
155
+ <line x1="40" y1="616" x2="1240" y2="616" stroke="#101828" stroke-opacity=".14" stroke-width=".8"/>
156
+ <text class="legend-title" x="40" y="652">LEGEND</text>
157
+ <line x1="144" y1="648" x2="184" y2="648" stroke="#4c5665" stroke-width="1.2" marker-end="url(#push-encryption-arrow)"/>
158
+ <text class="legend-text" x="196" y="652">LOCAL CONTENT</text>
159
+ <line x1="344" y1="648" x2="384" y2="648" stroke="#1d4ed8" stroke-width="1.2" marker-end="url(#push-encryption-arrow-accent)"/>
160
+ <text class="legend-text" x="396" y="652">ENCRYPTED CONTENT</text>
161
+ <rect x="568" y="640" width="16" height="16" rx="4" fill="#1d4ed8" fill-opacity=".08" stroke="#1d4ed8" stroke-width="1"/>
162
+ <text class="legend-text" x="596" y="652">ENCRYPTION BOUNDARY</text>
163
+ </svg>
164
+ </figure>
165
+ </body>
166
+ </html>
@@ -0,0 +1,148 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="push-encryption-flow-title push-encryption-flow-desc">
3
+ <title id="push-encryption-flow-title">How pi-bark encrypts every push</title>
4
+ <desc id="push-encryption-flow-desc">Architecture diagram showing copyb messages and automatic Pi status events entering one shared sender. When the config contains a custom key, every push uses AES256-GCM with a fresh 12-byte IV before Bark forwards the ciphertext to the app for local decryption.</desc>
5
+ <defs>
6
+ <style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap');</style>
7
+ <style>
8
+ .eyebrow { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
9
+ .heading { fill: #101828; font: 400 28px 'Instrument Serif', serif; }
10
+ .intro { fill: #4c5665; font: 400 12px 'Geist', sans-serif; }
11
+ .zone { fill: #e6e4de; stroke: #101828; stroke-opacity: .14; stroke-width: 1; }
12
+ .zone-label { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
13
+ .boundary { fill: none; stroke: #1d4ed8; stroke-opacity: .50; stroke-width: 1; stroke-dasharray: 4 4; }
14
+ .boundary-label { fill: #1d4ed8; font: 500 8px 'Geist Mono', monospace; letter-spacing: .12em; }
15
+ .node-mask { fill: #f0eee9; }
16
+ .node { fill: #ffffff; stroke: #101828; stroke-width: 1; }
17
+ .node-input { fill: #4c5665; fill-opacity: .10; stroke: #6a7282; stroke-width: 1; }
18
+ .node-store { fill: #101828; fill-opacity: .05; stroke: #4c5665; stroke-width: 1; }
19
+ .node-external { fill: #101828; fill-opacity: .03; stroke: #101828; stroke-opacity: .30; stroke-width: 1; }
20
+ .node-focal { fill: #1d4ed8; fill-opacity: .08; stroke: #1d4ed8; stroke-width: 1.2; }
21
+ .tag { fill: none; stroke: #101828; stroke-opacity: .34; stroke-width: .8; }
22
+ .tag-focal { fill: none; stroke: #1d4ed8; stroke-opacity: .55; stroke-width: .8; }
23
+ .tag-text { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .08em; text-anchor: middle; }
24
+ .tag-text-focal { fill: #1d4ed8; font: 500 8px 'Geist Mono', monospace; letter-spacing: .08em; text-anchor: middle; }
25
+ .name { fill: #101828; font: 600 12px 'Geist', sans-serif; text-anchor: middle; }
26
+ .sub { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; text-anchor: middle; }
27
+ .sub-focal { fill: #1d4ed8; font: 500 8px 'Geist Mono', monospace; text-anchor: middle; }
28
+ .edge { fill: none; stroke: #4c5665; stroke-width: 1.2; marker-end: url(#push-encryption-arrow); }
29
+ .edge-secure { fill: none; stroke: #1d4ed8; stroke-width: 1.2; marker-end: url(#push-encryption-arrow-accent); }
30
+ .label-mask { fill: #f0eee9; }
31
+ .label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; letter-spacing: .04em; text-anchor: middle; }
32
+ .label-secure { fill: #1d4ed8; font: 500 8px 'Geist Mono', monospace; letter-spacing: .04em; text-anchor: middle; }
33
+ .legend-title { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
34
+ .legend-text { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; }
35
+ </style>
36
+ <marker id="push-encryption-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
37
+ <polygon points="0 0,8 3,0 6" fill="#4c5665"/>
38
+ </marker>
39
+ <marker id="push-encryption-arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
40
+ <polygon points="0 0,8 3,0 6" fill="#1d4ed8"/>
41
+ </marker>
42
+ <marker id="push-encryption-arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
43
+ <polygon points="0 0,8 3,0 6" fill="#1d4ed8"/>
44
+ </marker>
45
+ </defs>
46
+
47
+ <rect width="1280" height="720" fill="#f0eee9"/>
48
+
49
+ <text class="eyebrow" x="40" y="48">ARCHITECTURE · PI-BARK</text>
50
+ <text class="heading" x="40" y="84">One encryption path for every Bark push</text>
51
+ <text class="intro" x="40" y="112">A configured key applies to manual output and automatic status notifications.</text>
52
+
53
+ <!-- Local trust zone and encrypted transport boundary -->
54
+ <rect class="zone" x="40" y="152" width="816" height="416" rx="8"/>
55
+ <rect class="label-mask" x="52" y="156" width="80" height="16" rx="2"/>
56
+ <text class="zone-label" x="60" y="168">LOCAL PI</text>
57
+
58
+ <line class="boundary" x1="880" y1="176" x2="880" y2="544"/>
59
+ <rect class="label-mask" x="892" y="180" width="240" height="16" rx="2"/>
60
+ <text class="boundary-label" x="900" y="192">CIPHERTEXT LEAVES PI</text>
61
+
62
+ <!-- Connectors are drawn before nodes -->
63
+ <path class="edge" d="M252 248 H292 Q300 248 300 256 V308 Q300 316 308 316 H352"/>
64
+ <path class="edge" d="M252 400 H304 Q312 400 312 392 V372 Q312 364 320 364 H352"/>
65
+ <line class="edge" x1="452" y1="452" x2="452" y2="396"/>
66
+ <line class="edge-secure" x1="552" y1="340" x2="648" y2="340"/>
67
+ <line class="edge-secure" x1="848" y1="340" x2="920" y2="340"/>
68
+ <line class="edge-secure" x1="1060" y1="340" x2="1100" y2="340"/>
69
+
70
+ <!-- Connector labels -->
71
+ <rect class="label-mask" x="464" y="416" width="72" height="12" rx="2"/>
72
+ <text class="label" x="500" y="424">LOAD EACH PUSH</text>
73
+
74
+ <rect class="label-mask" x="568" y="320" width="64" height="12" rx="2"/>
75
+ <text class="label-secure" x="600" y="328">KEY PRESENT</text>
76
+
77
+ <rect class="label-mask" x="856" y="320" width="56" height="12" rx="2"/>
78
+ <text class="label-secure" x="884" y="328">POST /PUSH</text>
79
+
80
+ <!-- Opaque node masks -->
81
+ <rect class="node-mask" x="72" y="208" width="180" height="80" rx="6"/>
82
+ <rect class="node-mask" x="72" y="360" width="180" height="80" rx="6"/>
83
+ <rect class="node-mask" x="352" y="284" width="200" height="112" rx="6"/>
84
+ <rect class="node-mask" x="352" y="452" width="200" height="80" rx="6"/>
85
+ <rect class="node-mask" x="648" y="284" width="200" height="112" rx="6"/>
86
+ <rect class="node-mask" x="920" y="284" width="140" height="112" rx="6"/>
87
+ <rect class="node-mask" x="1100" y="284" width="140" height="112" rx="6"/>
88
+
89
+ <!-- Inputs -->
90
+ <rect class="node-input" x="72" y="208" width="180" height="80" rx="6"/>
91
+ <rect class="tag" x="84" y="220" width="56" height="16" rx="2"/>
92
+ <text class="tag-text" x="112" y="232">COMMAND</text>
93
+ <text class="name" x="162" y="256">/copyb</text>
94
+ <text class="sub" x="162" y="276">LAST AGENT MESSAGE</text>
95
+
96
+ <rect class="node-input" x="72" y="360" width="180" height="80" rx="6"/>
97
+ <rect class="tag" x="84" y="372" width="48" height="16" rx="2"/>
98
+ <text class="tag-text" x="108" y="384">EVENT</text>
99
+ <text class="name" x="162" y="408">Pi status</text>
100
+ <text class="sub" x="162" y="428">NEEDS INPUT · FINISHED</text>
101
+
102
+ <!-- Shared sender and config -->
103
+ <rect class="node" x="352" y="284" width="200" height="112" rx="6"/>
104
+ <rect class="tag" x="364" y="296" width="48" height="16" rx="2"/>
105
+ <text class="tag-text" x="388" y="308">SHARED</text>
106
+ <text class="name" x="452" y="340">Push sender</text>
107
+ <text class="sub" x="452" y="364">ONE PATH FOR EVERY PUSH</text>
108
+ <text class="sub" x="452" y="380">DEVICE KEY + CONTENT</text>
109
+
110
+ <rect class="node-store" x="352" y="452" width="200" height="80" rx="6"/>
111
+ <rect class="tag" x="364" y="464" width="48" height="16" rx="2"/>
112
+ <text class="tag-text" x="388" y="476">CONFIG</text>
113
+ <text class="name" x="452" y="500">pi-bark config</text>
114
+ <text class="sub" x="452" y="520">DEVICE KEY · CUSTOM KEY</text>
115
+
116
+ <!-- Encryption focal point -->
117
+ <rect class="node-focal" x="648" y="284" width="200" height="112" rx="6"/>
118
+ <rect class="tag-focal" x="660" y="296" width="56" height="16" rx="2"/>
119
+ <text class="tag-text-focal" x="688" y="308">ENCRYPT</text>
120
+ <text class="name" x="748" y="340">AES256-GCM</text>
121
+ <text class="sub-focal" x="748" y="364">FRESH 12-BYTE IV</text>
122
+ <text class="sub-focal" x="748" y="380">AUTH TAG APPENDED</text>
123
+
124
+ <!-- External delivery -->
125
+ <rect class="node-external" x="920" y="284" width="140" height="112" rx="6"/>
126
+ <rect class="tag" x="932" y="296" width="48" height="16" rx="2"/>
127
+ <text class="tag-text" x="956" y="308">SERVER</text>
128
+ <text class="name" x="990" y="340">Bark server</text>
129
+ <text class="sub" x="990" y="364">FORWARDS ONLY</text>
130
+ <text class="sub" x="990" y="380">CANNOT READ CONTENT</text>
131
+
132
+ <rect class="node" x="1100" y="284" width="140" height="112" rx="6"/>
133
+ <rect class="tag" x="1112" y="296" width="32" height="16" rx="2"/>
134
+ <text class="tag-text" x="1128" y="308">APP</text>
135
+ <text class="name" x="1170" y="340">Bark App</text>
136
+ <text class="sub" x="1170" y="364">MATCHING 32-BYTE KEY</text>
137
+ <text class="sub" x="1170" y="380">DECRYPTS LOCALLY</text>
138
+
139
+ <!-- Legend -->
140
+ <line x1="40" y1="616" x2="1240" y2="616" stroke="#101828" stroke-opacity=".14" stroke-width=".8"/>
141
+ <text class="legend-title" x="40" y="652">LEGEND</text>
142
+ <line x1="144" y1="648" x2="184" y2="648" stroke="#4c5665" stroke-width="1.2" marker-end="url(#push-encryption-arrow)"/>
143
+ <text class="legend-text" x="196" y="652">LOCAL CONTENT</text>
144
+ <line x1="344" y1="648" x2="384" y2="648" stroke="#1d4ed8" stroke-width="1.2" marker-end="url(#push-encryption-arrow-accent)"/>
145
+ <text class="legend-text" x="396" y="652">ENCRYPTED CONTENT</text>
146
+ <rect x="568" y="640" width="16" height="16" rx="4" fill="#1d4ed8" fill-opacity=".08" stroke="#1d4ed8" stroke-width="1"/>
147
+ <text class="legend-text" x="596" y="652">ENCRYPTION BOUNDARY</text>
148
+ </svg>
@@ -3,7 +3,6 @@ import { copyToClipboard, type ExtensionAPI, type SessionEntry } from "@earendil
3
3
  import {
4
4
  createBarkConfigStore,
5
5
  DEFAULT_SERVER_URL,
6
- parseDeviceKey,
7
6
  parseServerUrl,
8
7
  statusNotificationsEnabled,
9
8
  type BarkConfig,
@@ -68,28 +67,22 @@ export default function barkExtension(pi: ExtensionAPI, options: BarkExtensionOp
68
67
 
69
68
  let statusPushQueue = Promise.resolve();
70
69
  let pendingFinishedPush: AbortController | undefined;
71
- const queueStatus = (
70
+ const queueStatus = async (
72
71
  title: string,
73
72
  cwd: string,
74
73
  shouldSend?: Promise<boolean>,
75
74
  cancelSignal?: AbortSignal,
76
75
  ): Promise<void> => {
77
- let configSnapshot: BarkConfig;
78
- try {
79
- configSnapshot = configStore.loadSync().value;
80
- } catch (error) {
81
- return Promise.reject(error);
82
- }
83
- if (!configSnapshot.deviceKey || !statusNotificationsEnabled(configSnapshot, cwd)) return Promise.resolve();
76
+ const configSnapshot = configStore.loadSync().value;
77
+ if (!configSnapshot.deviceKey || !statusNotificationsEnabled(configSnapshot, cwd)) return;
84
78
  const sessionName = pi.getSessionName()?.trim() || "Unnamed";
85
79
  const contentSnapshot = { title, body: `Pi session: ${sessionName}` };
86
80
  const send = async () => {
87
81
  if (shouldSend && !(await shouldSend)) return;
88
82
  await sendPush(configSnapshot, contentSnapshot, fetchImpl, cancelSignal);
89
83
  };
90
- const push = statusPushQueue.then(send, send);
91
- statusPushQueue = push;
92
- return push;
84
+ statusPushQueue = statusPushQueue.then(send, send);
85
+ return statusPushQueue;
93
86
  };
94
87
 
95
88
  pi.on("ui_prompt_start", (_event, ctx) => {
@@ -120,12 +113,12 @@ export default function barkExtension(pi: ExtensionAPI, options: BarkExtensionOp
120
113
  });
121
114
  });
122
115
 
123
- pi.registerCommand("bark-notifications", {
116
+ pi.registerCommand("bark", {
124
117
  description: "Configure automatic Bark status notifications",
125
118
  handler: async (args, ctx) => {
126
119
  const [first, second, extra] = args.trim().split(/\s+/);
127
120
  if (extra || (first === "default" ? !["on", "off"].includes(second) : second)) {
128
- throw new Error("Usage: /bark-notifications <on|off|inherit> | default <on|off>");
121
+ throw new Error("Usage: /bark <on|off|inherit> | default <on|off>");
129
122
  }
130
123
  if (first === "default") {
131
124
  const enabled = second === "on";
@@ -137,7 +130,7 @@ export default function barkExtension(pi: ExtensionAPI, options: BarkExtensionOp
137
130
  return;
138
131
  }
139
132
  if (!(["on", "off", "inherit"] as const).includes(first as "on" | "off" | "inherit")) {
140
- throw new Error("Usage: /bark-notifications <on|off|inherit> | default <on|off>");
133
+ throw new Error("Usage: /bark <on|off|inherit> | default <on|off>");
141
134
  }
142
135
  const cwd = resolve(ctx.cwd);
143
136
  await configStore.update((config) => {
@@ -182,7 +175,7 @@ export default function barkExtension(pi: ExtensionAPI, options: BarkExtensionOp
182
175
  await configStore.update((config) => ({
183
176
  ...config,
184
177
  serverUrl: parseServerUrl(serverUrl),
185
- deviceKey: parseDeviceKey(deviceKey),
178
+ deviceKey,
186
179
  }));
187
180
  ctx.ui.notify("Saved the Bark Device Key and server URL.", "info");
188
181
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-bark",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Send Pi session status and agent output to Bark on iOS.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -17,8 +17,8 @@
17
17
  "files": [
18
18
  "LICENSE",
19
19
  "dist",
20
+ "docs",
20
21
  "extensions",
21
- "scripts",
22
22
  "src",
23
23
  "README.md"
24
24
  ],
@@ -27,7 +27,6 @@
27
27
  },
28
28
  "scripts": {
29
29
  "build": "tsc --project tsconfig.build.json",
30
- "generate:key": "npm run build && node dist/scripts/generate-encryption-key.js",
31
30
  "test": "npm run build && node --test test/*.test.ts",
32
31
  "typecheck": "tsc --noEmit --allowImportingTsExtensions --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck extensions/bark.ts scripts/*.ts src/*.ts test/*.test.ts",
33
32
  "prepack": "npm run build",
package/src/encryption.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import { createCipheriv, randomBytes } from "node:crypto";
2
2
  import { ENCRYPTION_KEY_BYTES, type BarkEncryption } from "./config.ts";
3
3
 
4
- const RANDOM_TEXT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5
4
  const GCM_IV_BYTES = 12;
6
5
 
7
6
  function randomText(length: number): string {
8
- return Array.from(randomBytes(length), (byte) => RANDOM_TEXT_ALPHABET[byte & 63]).join("");
7
+ return randomBytes(length).toString("base64url").slice(0, length);
9
8
  }
10
9
 
11
10
  export function generateEncryptionKey(): string {
@@ -1,52 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import {
4
- createBarkConfigStore,
5
- ENCRYPTION_ALGORITHM,
6
- ENCRYPTION_MODE,
7
- ENCRYPTION_PADDING,
8
- } from "../src/config.ts";
9
- import { generateEncryptionKey } from "../src/encryption.ts";
10
-
11
- const args = process.argv.slice(2);
12
- const force = args.length === 1 && args[0] === "--force";
13
- const disable = args.length === 1 && args[0] === "--disable";
14
-
15
- if (args.length > 1 || (args.length === 1 && !force && !disable)) {
16
- console.error("Usage: pi-bark-key [--force | --disable]");
17
- process.exitCode = 1;
18
- } else {
19
- const store = createBarkConfigStore();
20
- try {
21
- if (disable) {
22
- await store.update((config) => ({ ...config, encryption: null }));
23
- console.log("Disabled Bark push encryption.");
24
- } else {
25
- let key = "";
26
- await store.update((config) => {
27
- if (config.encryption && !force) {
28
- throw new Error("A Bark Custom Encryption Key already exists. Use --force to replace it.");
29
- }
30
- key = generateEncryptionKey();
31
- return {
32
- ...config,
33
- encryption: {
34
- algorithm: ENCRYPTION_ALGORITHM,
35
- mode: ENCRYPTION_MODE,
36
- padding: ENCRYPTION_PADDING,
37
- key,
38
- },
39
- };
40
- });
41
- console.log("Generated and saved a Bark Custom Encryption Key.");
42
- console.log(`Algorithm: ${ENCRYPTION_ALGORITHM}`);
43
- console.log(`Mode: ${ENCRYPTION_MODE}`);
44
- console.log(`Padding: ${ENCRYPTION_PADDING}`);
45
- console.log(`Key: ${key}`);
46
- console.log("Enter these settings in Bark App > Push Encryption.");
47
- }
48
- } catch (error) {
49
- console.error(error instanceof Error ? error.message : String(error));
50
- process.exitCode = 1;
51
- }
52
- }