@henryqw/pi-bark 0.1.2
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/LICENSE +21 -0
- package/README.md +184 -0
- package/dist/scripts/generate-encryption-key.js +47 -0
- package/dist/src/config.js +111 -0
- package/dist/src/encryption.js +20 -0
- package/extensions/bark.ts +190 -0
- package/package.json +58 -0
- package/scripts/generate-encryption-key.ts +52 -0
- package/src/config.ts +141 -0
- package/src/encryption.ts +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henry Wang
|
|
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,184 @@
|
|
|
1
|
+
# `@henryqw/pi-bark`
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@henryqw/pi-bark
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Setup
|
|
12
|
+
|
|
13
|
+
1. Open the Bark App and copy its test URL.
|
|
14
|
+
2. Take the Device Key from the URL.
|
|
15
|
+
3. Run `/set-bark` in Pi.
|
|
16
|
+
|
|
17
|
+
For Bark's public server:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
/set-bark your-device-key
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For a self-hosted Bark server:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
/set-bark your-device-key https://bark.example.com
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A test URL such as `https://api.day.app/your-device-key/Test` contains the Device Key after the host. Do not include sample push content such as `/Test`.
|
|
30
|
+
|
|
31
|
+
Treat the Device Key as a secret. Anyone with it can send push notifications to your device.
|
|
32
|
+
|
|
33
|
+
## Use
|
|
34
|
+
|
|
35
|
+
Run `/copyb` with no arguments.
|
|
36
|
+
|
|
37
|
+
The command copies the last agent message to your clipboard. It then sends a Bark V2 push request to `<serverUrl>/push`.
|
|
38
|
+
|
|
39
|
+
Without push encryption, the JSON request uses Bark's standard parameter names:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"device_key": "your-device-key",
|
|
44
|
+
"body": "the last agent message"
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `body` value is the same text that Pi's `/copy` command selects. Markdown, code blocks, spacing, and line breaks stay unchanged.
|
|
49
|
+
|
|
50
|
+
| Surface | Type | Purpose |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `/copyb` | command | Copy the last agent message and send a Bark push notification. |
|
|
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. |
|
|
56
|
+
| `pi-bark-key [--force \| --disable]` | executable | Generate, replace, or disable the Custom Encryption Key. |
|
|
57
|
+
|
|
58
|
+
### Automatic status notifications
|
|
59
|
+
|
|
60
|
+
After setup, pi-bark sends two status-only notifications:
|
|
61
|
+
|
|
62
|
+
- **Pi needs input** when Pi opens a blocking user prompt, including `ask_question`.
|
|
63
|
+
- **Pi finished** after the agent has fully settled and will not continue automatically.
|
|
64
|
+
|
|
65
|
+
Each notification includes Pi's current session name. Herdr shows the same name when `pi-herdr-rename` is active. pi-bark does not call Herdr or read Herdr state.
|
|
66
|
+
|
|
67
|
+
An unset session name appears as `Unnamed`. Status notifications do not include prompts or agent output. No notification is sent when a prompt closes.
|
|
68
|
+
|
|
69
|
+
Automatic notifications are enabled by default. Disable them only in the current CWD:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
/bark-notifications off
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Use `on` to enable the current CWD explicitly. Use `inherit` to remove its override. The CWD then follows the global default.
|
|
76
|
+
|
|
77
|
+
Change that default for all CWDs without an override:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
/bark-notifications default off
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
These settings do not affect `/copyb`. CWD overrides live in pi-bark's global config, not in project files.
|
|
84
|
+
|
|
85
|
+
## Flow
|
|
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
|
+
```
|
|
96
|
+
|
|
97
|
+
## Config
|
|
98
|
+
|
|
99
|
+
Package-owned: `~/.pi/agent/config/pi-bark/config.json`
|
|
100
|
+
|
|
101
|
+
| Name | Description | Values | Default |
|
|
102
|
+
| --- | --- | --- | --- |
|
|
103
|
+
| `serverUrl` | Base URL of the Bark server that accepts push requests. | HTTP or HTTPS URL with an optional path and no query or fragment. | `"https://api.day.app"` |
|
|
104
|
+
| `deviceKey` | Identifies the Bark App installation that receives the push notification. | Non-empty string or `null`. | `null` |
|
|
105
|
+
| `encryption` | Custom Encryption Key and matching Bark encryption settings. | AES256-GCM settings object or `null`. | `null` |
|
|
106
|
+
| `statusNotifications` | Controls automatic status notifications by CWD. | Global boolean default and absolute-CWD boolean overrides. | Enabled with no overrides. |
|
|
107
|
+
|
|
108
|
+
### Push encryption
|
|
109
|
+
|
|
110
|
+
Push encryption is optional. It prevents the Bark server and Apple Push Notification service from reading the push content.
|
|
111
|
+
|
|
112
|
+
Generate and store a Custom Encryption Key:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npx @henryqw/pi-bark
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The script prints the generated key once. Enter these exact settings in **Bark App → Push Encryption**:
|
|
119
|
+
|
|
120
|
+
| Setting | Value |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| Algorithm | `AES256` |
|
|
123
|
+
| Mode | `GCM` |
|
|
124
|
+
| Padding | `noPadding` |
|
|
125
|
+
| Key | The generated 32-character key |
|
|
126
|
+
|
|
127
|
+
Do not paste script output into shared logs. The Custom Encryption Key can decrypt your push content.
|
|
128
|
+
|
|
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.
|
|
130
|
+
|
|
131
|
+
The GCM authentication tag is appended to the ciphertext before Base64 encoding, as Bark requires.
|
|
132
|
+
|
|
133
|
+
The script protects an existing Custom Encryption Key. Replace it only when you intend to update the Bark App too:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx @henryqw/pi-bark --force
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Disable push encryption in pi-bark with:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx @henryqw/pi-bark --disable
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Disable Push Encryption in the Bark App too. Otherwise its settings no longer match pi-bark.
|
|
146
|
+
|
|
147
|
+
The config file has this shape:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"serverUrl": "https://api.day.app",
|
|
152
|
+
"deviceKey": "your-device-key",
|
|
153
|
+
"encryption": {
|
|
154
|
+
"algorithm": "AES256",
|
|
155
|
+
"mode": "GCM",
|
|
156
|
+
"padding": "noPadding",
|
|
157
|
+
"key": "your-32-character-custom-key"
|
|
158
|
+
},
|
|
159
|
+
"statusNotifications": {
|
|
160
|
+
"defaultEnabled": true,
|
|
161
|
+
"cwdOverrides": {
|
|
162
|
+
"/srv/quiet-project": false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Set `encryption` to `null` when push encryption is disabled.
|
|
169
|
+
|
|
170
|
+
- A missing config stays missing until a command or the key script writes it.
|
|
171
|
+
- `/set-bark` preserves Push Encryption and status notification settings.
|
|
172
|
+
- The server URL must use HTTP or HTTPS. It cannot contain a query or fragment delimiter.
|
|
173
|
+
- Config writes are private and atomic.
|
|
174
|
+
- Pi commands never print the Device Key, server URL, or Custom Encryption Key.
|
|
175
|
+
|
|
176
|
+
## Limits and recovery
|
|
177
|
+
|
|
178
|
+
Bark and Apple Push Notification service limit notification payload sizes. This extension does not truncate push content. Bark returns an error when a message is too large.
|
|
179
|
+
|
|
180
|
+
`/copyb` waits up to 15 seconds for the Bark server. If the push request fails after the clipboard copy, your clipboard still contains the message.
|
|
181
|
+
|
|
182
|
+
Invalid config files cause a visible error and remain unchanged. Fix or move the malformed file, then retry the command.
|
|
183
|
+
|
|
184
|
+
See Bark's [Push Encryption documentation](https://github.com/Finb/Bark/blob/master/docs/en-us/encryption.md) for the encryption model.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createBarkConfigStore, ENCRYPTION_ALGORITHM, ENCRYPTION_MODE, ENCRYPTION_PADDING, } from "../src/config.js";
|
|
3
|
+
import { generateEncryptionKey } from "../src/encryption.js";
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
const force = args.length === 1 && args[0] === "--force";
|
|
6
|
+
const disable = args.length === 1 && args[0] === "--disable";
|
|
7
|
+
if (args.length > 1 || (args.length === 1 && !force && !disable)) {
|
|
8
|
+
console.error("Usage: pi-bark-key [--force | --disable]");
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
const store = createBarkConfigStore();
|
|
13
|
+
try {
|
|
14
|
+
if (disable) {
|
|
15
|
+
await store.update((config) => ({ ...config, encryption: null }));
|
|
16
|
+
console.log("Disabled Bark push encryption.");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
let key = "";
|
|
20
|
+
await store.update((config) => {
|
|
21
|
+
if (config.encryption && !force) {
|
|
22
|
+
throw new Error("A Bark Custom Encryption Key already exists. Use --force to replace it.");
|
|
23
|
+
}
|
|
24
|
+
key = generateEncryptionKey();
|
|
25
|
+
return {
|
|
26
|
+
...config,
|
|
27
|
+
encryption: {
|
|
28
|
+
algorithm: ENCRYPTION_ALGORITHM,
|
|
29
|
+
mode: ENCRYPTION_MODE,
|
|
30
|
+
padding: ENCRYPTION_PADDING,
|
|
31
|
+
key,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
console.log("Generated and saved a Bark Custom Encryption Key.");
|
|
36
|
+
console.log(`Algorithm: ${ENCRYPTION_ALGORITHM}`);
|
|
37
|
+
console.log(`Mode: ${ENCRYPTION_MODE}`);
|
|
38
|
+
console.log(`Padding: ${ENCRYPTION_PADDING}`);
|
|
39
|
+
console.log(`Key: ${key}`);
|
|
40
|
+
console.log("Enter these settings in Bark App > Push Encryption.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { createConfigStore } from "@henryqw/pi-config-store";
|
|
3
|
+
export const DEFAULT_SERVER_URL = "https://api.day.app";
|
|
4
|
+
export const ENCRYPTION_ALGORITHM = "AES256";
|
|
5
|
+
export const ENCRYPTION_MODE = "GCM";
|
|
6
|
+
export const ENCRYPTION_PADDING = "noPadding";
|
|
7
|
+
export const ENCRYPTION_KEY_BYTES = 32;
|
|
8
|
+
export function parseServerUrl(value) {
|
|
9
|
+
const input = value.trim();
|
|
10
|
+
let url;
|
|
11
|
+
try {
|
|
12
|
+
url = new URL(input);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new Error("Bark server URL must be a valid HTTP or HTTPS URL.");
|
|
16
|
+
}
|
|
17
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
18
|
+
throw new Error("Bark server URL must use HTTP or HTTPS.");
|
|
19
|
+
}
|
|
20
|
+
if (input.includes("?") || input.includes("#")) {
|
|
21
|
+
throw new Error("Bark server URL must not include a query or fragment.");
|
|
22
|
+
}
|
|
23
|
+
return url.href.replace(/\/$/, "");
|
|
24
|
+
}
|
|
25
|
+
export function parseDeviceKey(value) {
|
|
26
|
+
if (value === null)
|
|
27
|
+
return null;
|
|
28
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
29
|
+
throw new Error('Invalid Bark config: "deviceKey" must be a non-empty string.');
|
|
30
|
+
}
|
|
31
|
+
return value.trim();
|
|
32
|
+
}
|
|
33
|
+
function parseEncryption(value) {
|
|
34
|
+
if (value === null)
|
|
35
|
+
return null;
|
|
36
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
37
|
+
throw new Error('Invalid Bark config: "encryption" must be an object or null.');
|
|
38
|
+
}
|
|
39
|
+
const encryption = value;
|
|
40
|
+
if (Object.keys(encryption).length !== 4 ||
|
|
41
|
+
encryption.algorithm !== ENCRYPTION_ALGORITHM ||
|
|
42
|
+
encryption.mode !== ENCRYPTION_MODE ||
|
|
43
|
+
encryption.padding !== ENCRYPTION_PADDING ||
|
|
44
|
+
typeof encryption.key !== "string" ||
|
|
45
|
+
Buffer.byteLength(encryption.key, "utf8") !== ENCRYPTION_KEY_BYTES) {
|
|
46
|
+
throw new Error(`Invalid Bark encryption config: expected ${ENCRYPTION_ALGORITHM}, ${ENCRYPTION_MODE}, ${ENCRYPTION_PADDING}, and a ${ENCRYPTION_KEY_BYTES}-byte UTF-8 key.`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
algorithm: ENCRYPTION_ALGORITHM,
|
|
50
|
+
mode: ENCRYPTION_MODE,
|
|
51
|
+
padding: ENCRYPTION_PADDING,
|
|
52
|
+
key: encryption.key,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function parseStatusNotifications(value) {
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
57
|
+
throw new Error('Invalid Bark config: "statusNotifications" must be an object.');
|
|
58
|
+
}
|
|
59
|
+
const notifications = value;
|
|
60
|
+
const overrides = notifications.cwdOverrides;
|
|
61
|
+
if (Object.keys(notifications).length !== 2 ||
|
|
62
|
+
typeof notifications.defaultEnabled !== "boolean" ||
|
|
63
|
+
!overrides ||
|
|
64
|
+
typeof overrides !== "object" ||
|
|
65
|
+
Array.isArray(overrides)) {
|
|
66
|
+
throw new Error('Invalid Bark config: expected "defaultEnabled" and "cwdOverrides" notification settings.');
|
|
67
|
+
}
|
|
68
|
+
const entries = Object.entries(overrides);
|
|
69
|
+
if (entries.some(([cwd, enabled]) => !isAbsolute(cwd) || typeof enabled !== "boolean")) {
|
|
70
|
+
throw new Error("Invalid Bark config: CWD notification overrides must map absolute paths to booleans.");
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
defaultEnabled: notifications.defaultEnabled,
|
|
74
|
+
cwdOverrides: Object.fromEntries(entries),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function parseBarkConfig(value) {
|
|
78
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
79
|
+
throw new Error("Invalid Bark config: expected Bark settings object.");
|
|
80
|
+
}
|
|
81
|
+
const config = value;
|
|
82
|
+
if (Object.keys(config).length !== 4 ||
|
|
83
|
+
typeof config.serverUrl !== "string" ||
|
|
84
|
+
!("deviceKey" in config) ||
|
|
85
|
+
!("encryption" in config) ||
|
|
86
|
+
!("statusNotifications" in config)) {
|
|
87
|
+
throw new Error("Invalid Bark config: expected server, device, encryption, and notification settings.");
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
serverUrl: parseServerUrl(config.serverUrl),
|
|
91
|
+
deviceKey: parseDeviceKey(config.deviceKey),
|
|
92
|
+
encryption: parseEncryption(config.encryption),
|
|
93
|
+
statusNotifications: parseStatusNotifications(config.statusNotifications),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function statusNotificationsEnabled(config, cwd) {
|
|
97
|
+
return config.statusNotifications.cwdOverrides[resolve(cwd)] ?? config.statusNotifications.defaultEnabled;
|
|
98
|
+
}
|
|
99
|
+
export function createBarkConfigStore(agentDir) {
|
|
100
|
+
return createConfigStore({
|
|
101
|
+
extensionId: "pi-bark",
|
|
102
|
+
agentDir,
|
|
103
|
+
defaults: () => ({
|
|
104
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
105
|
+
deviceKey: null,
|
|
106
|
+
encryption: null,
|
|
107
|
+
statusNotifications: { defaultEnabled: true, cwdOverrides: {} },
|
|
108
|
+
}),
|
|
109
|
+
parse: parseBarkConfig,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createCipheriv, randomBytes } from "node:crypto";
|
|
2
|
+
import { ENCRYPTION_KEY_BYTES } from "./config.js";
|
|
3
|
+
const RANDOM_TEXT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
4
|
+
const GCM_IV_BYTES = 12;
|
|
5
|
+
function randomText(length) {
|
|
6
|
+
return Array.from(randomBytes(length), (byte) => RANDOM_TEXT_ALPHABET[byte & 63]).join("");
|
|
7
|
+
}
|
|
8
|
+
export function generateEncryptionKey() {
|
|
9
|
+
return randomText(ENCRYPTION_KEY_BYTES);
|
|
10
|
+
}
|
|
11
|
+
export function encryptPushContent(content, encryption) {
|
|
12
|
+
const iv = randomText(GCM_IV_BYTES);
|
|
13
|
+
const cipher = createCipheriv("aes-256-gcm", Buffer.from(encryption.key, "utf8"), Buffer.from(iv, "utf8"));
|
|
14
|
+
const encrypted = Buffer.concat([
|
|
15
|
+
cipher.update(JSON.stringify(content), "utf8"),
|
|
16
|
+
cipher.final(),
|
|
17
|
+
cipher.getAuthTag(),
|
|
18
|
+
]);
|
|
19
|
+
return { ciphertext: encrypted.toString("base64"), iv };
|
|
20
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { copyToClipboard, type ExtensionAPI, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
createBarkConfigStore,
|
|
5
|
+
DEFAULT_SERVER_URL,
|
|
6
|
+
parseDeviceKey,
|
|
7
|
+
parseServerUrl,
|
|
8
|
+
statusNotificationsEnabled,
|
|
9
|
+
type BarkConfig,
|
|
10
|
+
} from "../src/config.ts";
|
|
11
|
+
import { encryptPushContent } from "../src/encryption.ts";
|
|
12
|
+
|
|
13
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
14
|
+
|
|
15
|
+
type BarkPushContent = { body: string } & Record<string, string>;
|
|
16
|
+
|
|
17
|
+
type BarkExtensionOptions = {
|
|
18
|
+
agentDir?: string;
|
|
19
|
+
copy?: (text: string) => Promise<void>;
|
|
20
|
+
fetch?: typeof globalThis.fetch;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function lastAssistantText(entries: readonly SessionEntry[]): string | undefined {
|
|
24
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
25
|
+
const entry = entries[index];
|
|
26
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
27
|
+
const message = entry.message;
|
|
28
|
+
if (message.stopReason === "aborted" && message.content.length === 0) continue;
|
|
29
|
+
const text = message.content
|
|
30
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
31
|
+
.map((part) => part.text)
|
|
32
|
+
.join("")
|
|
33
|
+
.trim();
|
|
34
|
+
if (text) return text;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function sendPush(
|
|
40
|
+
config: BarkConfig,
|
|
41
|
+
content: BarkPushContent,
|
|
42
|
+
fetchImpl: typeof globalThis.fetch,
|
|
43
|
+
cancelSignal?: AbortSignal,
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
if (!config.deviceKey) throw new Error("Bark Device Key is required.");
|
|
46
|
+
const payload = config.encryption ? encryptPushContent(content, config.encryption) : content;
|
|
47
|
+
let response: Response;
|
|
48
|
+
try {
|
|
49
|
+
response = await fetchImpl(`${config.serverUrl}/push`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
52
|
+
body: JSON.stringify({ device_key: config.deviceKey, ...payload }),
|
|
53
|
+
signal: cancelSignal
|
|
54
|
+
? AbortSignal.any([cancelSignal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)])
|
|
55
|
+
: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (cancelSignal?.aborted) return;
|
|
59
|
+
throw new Error(`Bark push request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
60
|
+
}
|
|
61
|
+
if (!response.ok) throw new Error(`Bark push request failed with HTTP ${response.status}.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default function barkExtension(pi: ExtensionAPI, options: BarkExtensionOptions = {}): void {
|
|
65
|
+
const configStore = createBarkConfigStore(options.agentDir);
|
|
66
|
+
const copy = options.copy ?? copyToClipboard;
|
|
67
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
68
|
+
|
|
69
|
+
let statusPushQueue = Promise.resolve();
|
|
70
|
+
let pendingFinishedPush: AbortController | undefined;
|
|
71
|
+
const queueStatus = (
|
|
72
|
+
title: string,
|
|
73
|
+
cwd: string,
|
|
74
|
+
shouldSend?: Promise<boolean>,
|
|
75
|
+
cancelSignal?: AbortSignal,
|
|
76
|
+
): 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();
|
|
84
|
+
const sessionName = pi.getSessionName()?.trim() || "Unnamed";
|
|
85
|
+
const contentSnapshot = { title, body: `Pi session: ${sessionName}` };
|
|
86
|
+
const send = async () => {
|
|
87
|
+
if (shouldSend && !(await shouldSend)) return;
|
|
88
|
+
await sendPush(configSnapshot, contentSnapshot, fetchImpl, cancelSignal);
|
|
89
|
+
};
|
|
90
|
+
const push = statusPushQueue.then(send, send);
|
|
91
|
+
statusPushQueue = push;
|
|
92
|
+
return push;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
pi.on("ui_prompt_start", (_event, ctx) => {
|
|
96
|
+
void queueStatus("Pi needs input", ctx.cwd).catch((error) => {
|
|
97
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
pi.on("agent_start", () => {
|
|
102
|
+
pendingFinishedPush?.abort();
|
|
103
|
+
pendingFinishedPush = undefined;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
107
|
+
if (!ctx.isIdle()) return;
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
pendingFinishedPush?.abort();
|
|
110
|
+
pendingFinishedPush = controller;
|
|
111
|
+
const shouldSend = new Promise<boolean>((resolve) => {
|
|
112
|
+
setImmediate(() => resolve(ctx.isIdle()));
|
|
113
|
+
});
|
|
114
|
+
void queueStatus("Pi finished", ctx.cwd, shouldSend, controller.signal)
|
|
115
|
+
.catch((error) => {
|
|
116
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
|
|
117
|
+
})
|
|
118
|
+
.finally(() => {
|
|
119
|
+
if (pendingFinishedPush === controller) pendingFinishedPush = undefined;
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
pi.registerCommand("bark-notifications", {
|
|
124
|
+
description: "Configure automatic Bark status notifications",
|
|
125
|
+
handler: async (args, ctx) => {
|
|
126
|
+
const [first, second, extra] = args.trim().split(/\s+/);
|
|
127
|
+
if (extra || (first === "default" ? !["on", "off"].includes(second) : second)) {
|
|
128
|
+
throw new Error("Usage: /bark-notifications <on|off|inherit> | default <on|off>");
|
|
129
|
+
}
|
|
130
|
+
if (first === "default") {
|
|
131
|
+
const enabled = second === "on";
|
|
132
|
+
await configStore.update((config) => ({
|
|
133
|
+
...config,
|
|
134
|
+
statusNotifications: { ...config.statusNotifications, defaultEnabled: enabled },
|
|
135
|
+
}));
|
|
136
|
+
ctx.ui.notify(`${enabled ? "Enabled" : "Disabled"} Bark status notifications by default.`, "info");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
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>");
|
|
141
|
+
}
|
|
142
|
+
const cwd = resolve(ctx.cwd);
|
|
143
|
+
await configStore.update((config) => {
|
|
144
|
+
const cwdOverrides = { ...config.statusNotifications.cwdOverrides };
|
|
145
|
+
if (first === "inherit") delete cwdOverrides[cwd];
|
|
146
|
+
else cwdOverrides[cwd] = first === "on";
|
|
147
|
+
return {
|
|
148
|
+
...config,
|
|
149
|
+
statusNotifications: { ...config.statusNotifications, cwdOverrides },
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
ctx.ui.notify(
|
|
153
|
+
first === "inherit"
|
|
154
|
+
? "This CWD now inherits the default Bark notification setting."
|
|
155
|
+
: `${first === "on" ? "Enabled" : "Disabled"} Bark status notifications for this CWD.`,
|
|
156
|
+
"info",
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
pi.registerCommand("copyb", {
|
|
162
|
+
description: "Copy the last agent message and send a Bark push notification",
|
|
163
|
+
handler: async (args, ctx) => {
|
|
164
|
+
if (args.trim()) throw new Error("Usage: /copyb");
|
|
165
|
+
const config = configStore.loadSync().value;
|
|
166
|
+
if (!config.deviceKey) throw new Error("Bark is not configured. Run /set-bark <device-key> [server-url] first.");
|
|
167
|
+
|
|
168
|
+
const text = lastAssistantText(ctx.sessionManager.buildContextEntries());
|
|
169
|
+
if (!text) throw new Error("No agent messages to copy yet.");
|
|
170
|
+
|
|
171
|
+
await copy(text);
|
|
172
|
+
await sendPush(config, { body: text }, fetchImpl);
|
|
173
|
+
ctx.ui.notify("Copied the last agent message and sent a Bark push notification.", "info");
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
pi.registerCommand("set-bark", {
|
|
178
|
+
description: "Set the Bark Device Key and optional server URL",
|
|
179
|
+
handler: async (args, ctx) => {
|
|
180
|
+
const [deviceKey, serverUrl = DEFAULT_SERVER_URL, extra] = args.trim().split(/\s+/);
|
|
181
|
+
if (!deviceKey || extra) throw new Error("Usage: /set-bark <device-key> [server-url]");
|
|
182
|
+
await configStore.update((config) => ({
|
|
183
|
+
...config,
|
|
184
|
+
serverUrl: parseServerUrl(serverUrl),
|
|
185
|
+
deviceKey: parseDeviceKey(deviceKey),
|
|
186
|
+
}));
|
|
187
|
+
ctx.ui.notify("Saved the Bark Device Key and server URL.", "info");
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@henryqw/pi-bark",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Send Pi session status and agent output to Bark on iOS.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"bark",
|
|
9
|
+
"ios",
|
|
10
|
+
"notification"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.19.0"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"files": [
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"dist",
|
|
20
|
+
"extensions",
|
|
21
|
+
"scripts",
|
|
22
|
+
"src",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"pi-bark-key": "./dist/scripts/generate-encryption-key.js"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc --project tsconfig.build.json",
|
|
30
|
+
"generate:key": "npm run build && node dist/scripts/generate-encryption-key.js",
|
|
31
|
+
"test": "npm run build && node --test test/*.test.ts",
|
|
32
|
+
"typecheck": "tsc --noEmit --allowImportingTsExtensions --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck extensions/bark.ts scripts/*.ts src/*.ts test/*.test.ts",
|
|
33
|
+
"prepack": "npm run build",
|
|
34
|
+
"pack:check": "npm pack --dry-run"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-coding-agent": "^0.85.1"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/HenryQW/pi-harness.git",
|
|
42
|
+
"directory": "extensions/pi-bark"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/HenryQW/pi-harness/issues"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"pi": {
|
|
51
|
+
"extensions": [
|
|
52
|
+
"./extensions/bark.ts"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@henryqw/pi-config-store": "^1.1.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { createConfigStore } from "@henryqw/pi-config-store";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_SERVER_URL = "https://api.day.app";
|
|
5
|
+
export const ENCRYPTION_ALGORITHM = "AES256";
|
|
6
|
+
export const ENCRYPTION_MODE = "GCM";
|
|
7
|
+
export const ENCRYPTION_PADDING = "noPadding";
|
|
8
|
+
export const ENCRYPTION_KEY_BYTES = 32;
|
|
9
|
+
|
|
10
|
+
export type BarkEncryption = {
|
|
11
|
+
algorithm: typeof ENCRYPTION_ALGORITHM;
|
|
12
|
+
mode: typeof ENCRYPTION_MODE;
|
|
13
|
+
padding: typeof ENCRYPTION_PADDING;
|
|
14
|
+
key: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type BarkConfig = {
|
|
18
|
+
serverUrl: string;
|
|
19
|
+
deviceKey: string | null;
|
|
20
|
+
encryption: BarkEncryption | null;
|
|
21
|
+
statusNotifications: {
|
|
22
|
+
defaultEnabled: boolean;
|
|
23
|
+
cwdOverrides: Record<string, boolean>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function parseServerUrl(value: string): string {
|
|
28
|
+
const input = value.trim();
|
|
29
|
+
let url: URL;
|
|
30
|
+
try {
|
|
31
|
+
url = new URL(input);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error("Bark server URL must be a valid HTTP or HTTPS URL.");
|
|
34
|
+
}
|
|
35
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
36
|
+
throw new Error("Bark server URL must use HTTP or HTTPS.");
|
|
37
|
+
}
|
|
38
|
+
if (input.includes("?") || input.includes("#")) {
|
|
39
|
+
throw new Error("Bark server URL must not include a query or fragment.");
|
|
40
|
+
}
|
|
41
|
+
return url.href.replace(/\/$/, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseDeviceKey(value: unknown): string | null {
|
|
45
|
+
if (value === null) return null;
|
|
46
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
47
|
+
throw new Error('Invalid Bark config: "deviceKey" must be a non-empty string.');
|
|
48
|
+
}
|
|
49
|
+
return value.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseEncryption(value: unknown): BarkEncryption | null {
|
|
53
|
+
if (value === null) return null;
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
55
|
+
throw new Error('Invalid Bark config: "encryption" must be an object or null.');
|
|
56
|
+
}
|
|
57
|
+
const encryption = value as Record<string, unknown>;
|
|
58
|
+
if (
|
|
59
|
+
Object.keys(encryption).length !== 4 ||
|
|
60
|
+
encryption.algorithm !== ENCRYPTION_ALGORITHM ||
|
|
61
|
+
encryption.mode !== ENCRYPTION_MODE ||
|
|
62
|
+
encryption.padding !== ENCRYPTION_PADDING ||
|
|
63
|
+
typeof encryption.key !== "string" ||
|
|
64
|
+
Buffer.byteLength(encryption.key, "utf8") !== ENCRYPTION_KEY_BYTES
|
|
65
|
+
) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Invalid Bark encryption config: expected ${ENCRYPTION_ALGORITHM}, ${ENCRYPTION_MODE}, ${ENCRYPTION_PADDING}, and a ${ENCRYPTION_KEY_BYTES}-byte UTF-8 key.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
algorithm: ENCRYPTION_ALGORITHM,
|
|
72
|
+
mode: ENCRYPTION_MODE,
|
|
73
|
+
padding: ENCRYPTION_PADDING,
|
|
74
|
+
key: encryption.key,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseStatusNotifications(value: unknown): BarkConfig["statusNotifications"] {
|
|
79
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
80
|
+
throw new Error('Invalid Bark config: "statusNotifications" must be an object.');
|
|
81
|
+
}
|
|
82
|
+
const notifications = value as Record<string, unknown>;
|
|
83
|
+
const overrides = notifications.cwdOverrides;
|
|
84
|
+
if (
|
|
85
|
+
Object.keys(notifications).length !== 2 ||
|
|
86
|
+
typeof notifications.defaultEnabled !== "boolean" ||
|
|
87
|
+
!overrides ||
|
|
88
|
+
typeof overrides !== "object" ||
|
|
89
|
+
Array.isArray(overrides)
|
|
90
|
+
) {
|
|
91
|
+
throw new Error('Invalid Bark config: expected "defaultEnabled" and "cwdOverrides" notification settings.');
|
|
92
|
+
}
|
|
93
|
+
const entries = Object.entries(overrides);
|
|
94
|
+
if (entries.some(([cwd, enabled]) => !isAbsolute(cwd) || typeof enabled !== "boolean")) {
|
|
95
|
+
throw new Error("Invalid Bark config: CWD notification overrides must map absolute paths to booleans.");
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
defaultEnabled: notifications.defaultEnabled,
|
|
99
|
+
cwdOverrides: Object.fromEntries(entries) as Record<string, boolean>,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseBarkConfig(value: unknown): BarkConfig {
|
|
104
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
105
|
+
throw new Error("Invalid Bark config: expected Bark settings object.");
|
|
106
|
+
}
|
|
107
|
+
const config = value as Record<string, unknown>;
|
|
108
|
+
if (
|
|
109
|
+
Object.keys(config).length !== 4 ||
|
|
110
|
+
typeof config.serverUrl !== "string" ||
|
|
111
|
+
!("deviceKey" in config) ||
|
|
112
|
+
!("encryption" in config) ||
|
|
113
|
+
!("statusNotifications" in config)
|
|
114
|
+
) {
|
|
115
|
+
throw new Error("Invalid Bark config: expected server, device, encryption, and notification settings.");
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
serverUrl: parseServerUrl(config.serverUrl),
|
|
119
|
+
deviceKey: parseDeviceKey(config.deviceKey),
|
|
120
|
+
encryption: parseEncryption(config.encryption),
|
|
121
|
+
statusNotifications: parseStatusNotifications(config.statusNotifications),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function statusNotificationsEnabled(config: BarkConfig, cwd: string): boolean {
|
|
126
|
+
return config.statusNotifications.cwdOverrides[resolve(cwd)] ?? config.statusNotifications.defaultEnabled;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createBarkConfigStore(agentDir?: string) {
|
|
130
|
+
return createConfigStore<BarkConfig>({
|
|
131
|
+
extensionId: "pi-bark",
|
|
132
|
+
agentDir,
|
|
133
|
+
defaults: () => ({
|
|
134
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
135
|
+
deviceKey: null,
|
|
136
|
+
encryption: null,
|
|
137
|
+
statusNotifications: { defaultEnabled: true, cwdOverrides: {} },
|
|
138
|
+
}),
|
|
139
|
+
parse: parseBarkConfig,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createCipheriv, randomBytes } from "node:crypto";
|
|
2
|
+
import { ENCRYPTION_KEY_BYTES, type BarkEncryption } from "./config.ts";
|
|
3
|
+
|
|
4
|
+
const RANDOM_TEXT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
5
|
+
const GCM_IV_BYTES = 12;
|
|
6
|
+
|
|
7
|
+
function randomText(length: number): string {
|
|
8
|
+
return Array.from(randomBytes(length), (byte) => RANDOM_TEXT_ALPHABET[byte & 63]).join("");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function generateEncryptionKey(): string {
|
|
12
|
+
return randomText(ENCRYPTION_KEY_BYTES);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function encryptPushContent(
|
|
16
|
+
content: Readonly<Record<string, string>>,
|
|
17
|
+
encryption: BarkEncryption,
|
|
18
|
+
): { ciphertext: string; iv: string } {
|
|
19
|
+
const iv = randomText(GCM_IV_BYTES);
|
|
20
|
+
const cipher = createCipheriv("aes-256-gcm", Buffer.from(encryption.key, "utf8"), Buffer.from(iv, "utf8"));
|
|
21
|
+
const encrypted = Buffer.concat([
|
|
22
|
+
cipher.update(JSON.stringify(content), "utf8"),
|
|
23
|
+
cipher.final(),
|
|
24
|
+
cipher.getAuthTag(),
|
|
25
|
+
]);
|
|
26
|
+
return { ciphertext: encrypted.toString("base64"), iv };
|
|
27
|
+
}
|