bloby-bot 0.50.1 → 0.50.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.50.1",
3
+ "version": "0.50.2",
4
4
  "releaseNotes": [
5
5
  "1. Something great..",
6
6
  "2. ",
@@ -392,6 +392,7 @@ export async function startSupervisor() {
392
392
  'POST /api/channels/whatsapp/react',
393
393
  'POST /api/channels/send',
394
394
  'POST /api/channels/alexa/handle',
395
+ 'POST /api/whisper/transcribe-file',
395
396
  ];
396
397
 
397
398
  function isExemptRoute(method: string, url: string): boolean {
@@ -4,11 +4,11 @@
4
4
 
5
5
  A channel for getting **recordings off the user's Plaud Note device** and into your workspace as `(audio file, transcript)` pairs you can read and act on.
6
6
 
7
- Plaud is a tiny voice recorder (button on the case, magnet sticks to a phone). When the user records something — a meeting, a lecture, a thought on a walk — the device syncs to Plaud's cloud over Bluetooth/Wi-Fi. **You don't talk to the device.** You talk to Plaud's cloud, pull the audio, and transcribe it yourself.
7
+ Plaud is a tiny voice recorder. When the user records something — a meeting, a lecture, a thought on a walk — the device syncs to Plaud's cloud over Bluetooth/Wi-Fi. **You don't talk to the device.** You talk to Plaud's cloud, pull the audio, and transcribe it yourself.
8
8
 
9
9
  There is **no Plaud CLI, no Plaud webhook, no official Plaud API.** Plaud's mobile/web app uses an undocumented HTTP API. This skill uses the same one — same shape OpenPlaud uses (`https://github.com/openplaud/openplaud`).
10
10
 
11
- The user already pays Plaud $0 if they don't want Plaud's transcription subscription. We do transcription locally via Whisper using the OpenAI key the user added during the Bloby wizard. No new key, no new subscription.
11
+ The user already has Whisper enabled via the Bloby wizard. We use that OpenAI key no new key, no new subscription, no Plaud AI plan needed.
12
12
 
13
13
  ---
14
14
 
@@ -16,18 +16,36 @@ The user already pays Plaud $0 if they don't want Plaud's transcription subscrip
16
16
 
17
17
  | Thing | Where | How you use it |
18
18
  |---|---|---|
19
- | Whisper-on-disk endpoint | `POST http://localhost:7400/api/whisper/transcribe-file` | Send a path under `workspace/files/`, get a transcript back. Optional `saveTranscriptNext: true` writes `foo.mp3.txt` next to `foo.mp3`. |
20
- | Settings k/v store | `GET/POST/PUT http://localhost:7400/api/settings` | Store/retrieve the Plaud JWT, region, workspace ID, last-sync cursor. |
21
- | Workspace files dir | `workspace/files/audio/plaud/` | Drop downloaded audio here. The supervisor serves it at `/api/files/audio/plaud/<name>`. |
19
+ | Whisper-on-disk endpoint | `POST http://localhost:7400/api/whisper/transcribe-file` | Send a path under `workspace/files/`, get a transcript back. Optional `saveTranscriptNext: true` writes `foo.mp3.txt` next to `foo.mp3`. Auth-exempt, no Bearer needed. |
20
+ | Workspace files dir | `workspace/files/audio/plaud/` | Drop downloaded audio here. Supervisor serves it at `/api/files/audio/plaud/<name>`. |
21
+ | Workspace file tools | `Read` / `Write` / `Edit` | Store Plaud auth state in `workspace/.plaud.json` (see below). No `/api/settings` calls — that endpoint requires a portal Bearer token the skill can't easily produce. |
22
22
  | Scheduling | `workspace/CRONS.json` or `workspace/PULSE.json` | Run sync periodically. See "Cadence" below. |
23
23
 
24
- Use `http://localhost:7400` from Bash. Auth is the same Bearer token you already have in your session for the worker; for skill-internal calls running inside the supervisor's own bloby session, `/api/settings` and `/api/whisper/transcribe-file` work the same way `/api/whisper/transcribe` does.
24
+ Use `http://localhost:7400` from Bash for the Whisper endpoint. Everything else is the open internet (Plaud's API) or your own filesystem.
25
+
26
+ ### State file: `workspace/.plaud.json`
27
+
28
+ You manage all Plaud connection state in a single JSON file at workspace root. Read with `Read`, write with `Write`. Shape:
29
+
30
+ ```json
31
+ {
32
+ "email": "bruno@bertapeli.com",
33
+ "apiBase": "https://api.plaud.ai",
34
+ "userToken": "eyJ...",
35
+ "workspaceId": "ws_xxxxx",
36
+ "workspaceToken": "eyJ...",
37
+ "workspaceTokenMintedAt": "2026-05-22T19:30:00.000Z",
38
+ "lastSyncVersionMs": 1716412800000
39
+ }
40
+ ```
41
+
42
+ Initialize empty (`{}`) if the file doesn't exist. Never commit secrets — `.plaud.json` is gitignored by default (starts with `.`).
25
43
 
26
44
  ---
27
45
 
28
46
  ## Plaud's API in 60 seconds
29
47
 
30
- Three regions. Pick one when pairing. Token from one region won't work on another.
48
+ Three regions. Pick one when pairing. A token from one region won't work on another.
31
49
 
32
50
  | Region | Base URL |
33
51
  |---|---|
@@ -35,9 +53,16 @@ Three regions. Pick one when pairing. Token from one region won't work on anothe
35
53
  | EU | `https://api-euc1.plaud.ai` |
36
54
  | Asia-Pacific | `https://api-apse1.plaud.ai` |
37
55
 
38
- If the user doesn't know their region, start with Global. If `POST /auth/otp-send-code` returns `status: -302` with `data.domains.api`, redirect to that base instead — the user's account lives in a different region. Save whichever base actually succeeded.
56
+ If the user doesn't know their region, start with Global. If `POST /auth/otp-send-code` returns `status: -302` with `data.domains.api`, retry against that base — the user's account lives in a different region. Save whichever base actually succeeded.
39
57
 
40
- **User-Agent matters.** Plaud blocks some defaults. Always send a normal browser UA. Example:
58
+ **Two token kinds.** This is the part that bites everyone:
59
+
60
+ - **User Token (UT)** — what `/auth/otp-login` returns. Authenticates `/user/me`, the workspace-list endpoint, and the workspace-token mint endpoint. **It does NOT authenticate recording endpoints.** Calling `/file/simple/web` or `/device/list` with a UT silently returns HTTP 200 + empty list. This is exactly the "I have no recordings but my Plaud app shows 3 files" symptom.
61
+ - **Workspace Token (WT)** — minted from the UT. Required on all recording endpoints. ~24h lifetime. Re-mint when expired.
62
+
63
+ **You always need both.** UT lives long, WT is short-lived. Workflow: OTP → UT → list workspaces (with UT) → mint WT for the personal workspace (with UT) → use WT for everything recording-related.
64
+
65
+ **User-Agent matters.** Plaud blocks some defaults. Always send a normal browser UA:
41
66
 
42
67
  ```
43
68
  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
@@ -56,7 +81,7 @@ Bloby: Which email do you use on plaud.ai? I'll have them send you a 6-digit cod
56
81
  Human: bruno@example.com
57
82
  ```
58
83
 
59
- If the human mentions they signed up with **Google or Apple**, jump to the "Paste-token fallback" section instead. OTP only works for accounts that were created with an email+password identity on Plaud's side.
84
+ If the human mentions they signed up with **Google or Apple**, jump to the "Paste-token fallback" section OTP only works for email+password Plaud identities.
60
85
 
61
86
  ### Step 2 — Send the OTP
62
87
 
@@ -84,34 +109,81 @@ curl -s -X POST '<apiBase>/auth/otp-login' \
84
109
  -d '{"code":"<6 DIGITS>","token":"<OTP TOKEN FROM STEP 2>"}'
85
110
  ```
86
111
 
87
- Expected `access_token` (a long `eyJ...` JWT). **This is the long-lived token. Store it.**
112
+ Expected `access_token` (a long `eyJ...` JWT). **This is the User Token (UT). Save it as `userToken`.**
113
+
114
+ > ⚠️ **Don't be misled by `is_new_user: true`** in this response. It's an informational flag for the Plaud client — it does NOT mean Plaud just created a fresh account for you. Your real account is intact. The empty `data_devices: []` you'll see next is because UT can't read recording/device endpoints — that's the workspace-token issue, not "wrong account."
88
115
 
89
- ### Step 5 — Store the connection
116
+ ### Step 5 — Write initial state to `workspace/.plaud.json`
117
+
118
+ Use the `Write` tool. No `/api/settings` calls.
119
+
120
+ ```json
121
+ {
122
+ "email": "<EMAIL>",
123
+ "apiBase": "<BASE THAT WORKED>",
124
+ "userToken": "<UT FROM STEP 4>"
125
+ }
126
+ ```
127
+
128
+ ### Step 6 — Smoke test the UT (don't try `/device/list` yet)
90
129
 
91
130
  ```bash
92
- # Each setting saved separately. Replace <TOKEN> and <BASE>.
93
- curl -s -X POST 'http://localhost:7400/api/settings' \
94
- -H 'Content-Type: application/json' \
95
- -d '{"key":"plaud_token","value":"<JWT>"}'
96
- curl -s -X POST 'http://localhost:7400/api/settings' \
97
- -H 'Content-Type: application/json' \
98
- -d '{"key":"plaud_api_base","value":"<BASE>"}'
99
- curl -s -X POST 'http://localhost:7400/api/settings' \
131
+ curl -s '<BASE>/user/me' \
132
+ -H 'Authorization: Bearer <UT>' \
133
+ -H 'User-Agent: Mozilla/5.0 ...'
134
+ ```
135
+
136
+ Should return the user's profile (email matches the one you used to pair). If 401, the UT is bad — start over. If 200 but the email is different from what the human gave you, the OTP went to a different identity (Google/Apple collision) — explain and go to paste-token fallback.
137
+
138
+ ### Step 7 Mint the Workspace Token (REQUIRED)
139
+
140
+ This is the step that makes the difference between "0 recordings" and "all 3 of my recordings."
141
+
142
+ **7a. List workspaces** (auth: UT):
143
+
144
+ ```bash
145
+ curl -s '<BASE>/team-app/workspaces/list?need_personal_workspace=true' \
146
+ -H 'Authorization: Bearer <UT>' \
147
+ -H 'User-Agent: Mozilla/5.0 ...'
148
+ ```
149
+
150
+ Response shape: `{ status: 0, data: { workspaces: [{ workspace_id, workspace_type, ... }] } }`.
151
+
152
+ Pick the **personal** workspace — the one where `workspace_type === "0"`. If no workspace has type `"0"` (rare), use the first entry. Save its `workspace_id` as `workspaceId`.
153
+
154
+ **7b. Mint a WT for that workspace** (auth: UT, body is literally `{}`):
155
+
156
+ ```bash
157
+ curl -s -X POST '<BASE>/user-app/auth/workspace/token/<WORKSPACE_ID>' \
158
+ -H 'Authorization: Bearer <UT>' \
100
159
  -H 'Content-Type: application/json' \
101
- -d '{"key":"plaud_email","value":"<EMAIL>"}'
160
+ -H 'User-Agent: Mozilla/5.0 ...' \
161
+ -d '{}'
102
162
  ```
103
163
 
104
- You can also save `plaud_workspace_id` once you discover it (see "Workspaces" below).
164
+ Response: `{ status: 0, data: { workspace_token: "eyJ..." } }`.
105
165
 
106
- ### Step 6 Smoke test
166
+ **Save it** as `workspaceToken` and `workspaceTokenMintedAt: <now ISO 8601>` in `.plaud.json`. Now Update the file via `Write`.
167
+
168
+ ### Step 8 — Real smoke test (with WT)
107
169
 
108
170
  ```bash
109
171
  curl -s '<BASE>/device/list' \
110
- -H 'Authorization: Bearer <JWT>' \
172
+ -H 'Authorization: Bearer <WT>' \
111
173
  -H 'User-Agent: Mozilla/5.0 ...'
112
174
  ```
113
175
 
114
- Should return a JSON object listing the user's Plaud devices (each has a `serial_number`). If you get `401`, the OTP didn't grant a usable token — start over. If `200`, tell the human: *"Paired. Your Plaud (serial ending ...XXXX) is connected. Want me to pull in everything you've recorded so far?"*
176
+ Now you should see devices. Tell the human: *"Paired. Your Plaud (serial ending ...XXXX) is connected. Want me to pull in everything you've recorded so far?"*
177
+
178
+ If `data_devices` is still empty here — odd, but possible for accounts that haven't synced any device in a while. Try the recordings list directly:
179
+
180
+ ```bash
181
+ curl -s '<BASE>/file/simple/web?skip=0&limit=10&is_trash=0' \
182
+ -H 'Authorization: Bearer <WT>' \
183
+ -H 'User-Agent: Mozilla/5.0 ...'
184
+ ```
185
+
186
+ If `data_file_list` has entries, you're good — devices list can be empty even when recordings exist.
115
187
 
116
188
  ---
117
189
 
@@ -123,9 +195,8 @@ If OTP just won't work and the human signed up with Google or Apple, get the bea
123
195
  2. Open DevTools (F12 or Cmd+Option+I) → Network tab → refresh.
124
196
  3. Click any request to `api.plaud.ai`, `api-euc1.plaud.ai`, or `api-apse1.plaud.ai`.
125
197
  4. Under **Request Headers**, find `Authorization`. Copy everything after `Bearer ` (the long `eyJ...`).
126
- 5. Tell the bloby in chat. The bloby saves it via the same `plaud_token` setting key, plus the matching `plaud_api_base`.
127
-
128
- JWTs from this path expire too. The skill behaviour on 401 is the same (see "Re-auth" below).
198
+ 5. The human pastes it to you in chat. Save it as `userToken` and set `apiBase` to whichever host they pulled it from.
199
+ 6. **Still run Step 7** — mint a workspace token. The paste-token gives you a UT, same as OTP. WT is still required.
129
200
 
130
201
  ---
131
202
 
@@ -134,18 +205,22 @@ JWTs from this path expire too. The skill behaviour on 401 is the same (see "Re-
134
205
  The shape of a sync run:
135
206
 
136
207
  ```
137
- GET /file/simple/web → list recent recordings (paginated)
208
+ GET /file/simple/web → list recent recordings (paginated) [auth: WT]
138
209
  for each new one:
139
- GET /file/temp-url/<id>?is_opus=0 → get a short-lived S3 link (request the mp3, not opus)
140
- curl -o workspace/files/audio/plaud/<id>.mp3 → download
141
- POST /api/whisper/transcribe-file → produces <id>.mp3.txt alongside
210
+ GET /file/temp-url/<id>?is_opus=0 → get a short-lived S3 link [auth: WT]
211
+ curl -o workspace/files/audio/plaud/<id>.mp3 → download (no auth, signed URL)
212
+ POST /api/whisper/transcribe-file → produces <id>.mp3.txt alongside
142
213
  ```
143
214
 
144
- ### List recordings
215
+ ### Pre-sync: check WT freshness
216
+
217
+ Read `.plaud.json`. If `workspaceToken` is missing or `workspaceTokenMintedAt` is more than ~20 hours old, re-mint (Step 7b above) and update the file before starting the sync. WT lifetime is ~24h; refresh defensively.
218
+
219
+ ### List recordings (auth: WT)
145
220
 
146
221
  ```bash
147
222
  curl -s '<BASE>/file/simple/web?skip=0&limit=50&is_trash=0&sort_by=edit_time&is_desc=true' \
148
- -H 'Authorization: Bearer <JWT>' \
223
+ -H 'Authorization: Bearer <WT>' \
149
224
  -H 'User-Agent: Mozilla/5.0 ...'
150
225
  ```
151
226
 
@@ -161,37 +236,37 @@ The response has `data_file_list` — an array of recording objects. Fields you'
161
236
  | `serial_number` | Which Plaud device. |
162
237
  | `is_trash` | Skip if 1. |
163
238
 
164
- Page with `skip=` (the API also accepts a huge `limit`, but page through 50-at-a-time politely).
239
+ Page with `skip=`; do 50 at a time. Stop when a page comes back smaller than `limit` or empty.
165
240
 
166
241
  ### Dedup
167
242
 
168
243
  You don't want to re-download what you already have. Two ways, pick one:
169
244
 
170
245
  - **Filesystem**: if `workspace/files/audio/plaud/<id>.mp3` exists, skip it.
171
- - **Cursor**: save the newest `version_ms` you've seen as `plaud_last_sync` setting. On next sync, skip anything `<=` that cursor. Faster — no `ls` needed.
246
+ - **Cursor**: save the newest `version_ms` you've seen as `lastSyncVersionMs` in `.plaud.json`. Skip anything `<=` that cursor next time.
172
247
 
173
248
  If `version_ms` changed on a recording you already downloaded, the user edited the filename or trimmed it. Re-fetch and overwrite.
174
249
 
175
- ### Get the download URL
250
+ ### Get the download URL (auth: WT)
176
251
 
177
252
  ```bash
178
253
  curl -s '<BASE>/file/temp-url/<FILE_ID>?is_opus=0' \
179
- -H 'Authorization: Bearer <JWT>' \
254
+ -H 'Authorization: Bearer <WT>' \
180
255
  -H 'User-Agent: Mozilla/5.0 ...'
181
256
  ```
182
257
 
183
- `is_opus=0` returns the mp3 variant (`temp_url`). `is_opus=1` returns opus in `temp_url_opus`. **Use mp3** — Whisper handles it cleanly, opus needs ffmpeg.
258
+ `is_opus=0` returns mp3 in `temp_url`. `is_opus=1` returns opus in `temp_url_opus`. **Use mp3** — Whisper handles it natively, opus would need ffmpeg.
184
259
 
185
- Response: `{ "temp_url": "https://<s3...>" }`. The URL expires in a few minutes. Download immediately.
260
+ The URL expires in minutes. Download immediately.
186
261
 
187
- ### Download
262
+ ### Download (no auth — URL is signed)
188
263
 
189
264
  ```bash
190
265
  mkdir -p workspace/files/audio/plaud
191
266
  curl -s -o "workspace/files/audio/plaud/<FILE_ID>.mp3" '<TEMP URL>'
192
267
  ```
193
268
 
194
- ### Transcribe
269
+ ### Transcribe (no auth — endpoint is exempt)
195
270
 
196
271
  ```bash
197
272
  curl -s -X POST 'http://localhost:7400/api/whisper/transcribe-file' \
@@ -199,18 +274,15 @@ curl -s -X POST 'http://localhost:7400/api/whisper/transcribe-file' \
199
274
  -d '{"path":"audio/plaud/<FILE_ID>.mp3","saveTranscriptNext":true}'
200
275
  ```
201
276
 
202
- Returns `{ "transcript": "...", "transcriptPath": "audio/plaud/<FILE_ID>.mp3.txt" }`. The `.txt` file is now sitting next to the audio. You can read it with `Read` like any other file.
203
-
204
- The user's `whisper_key` from the wizard is what powers this — you don't need to know or handle the OpenAI key.
277
+ Returns `{ "transcript": "...", "transcriptPath": "audio/plaud/<FILE_ID>.mp3.txt" }`. The `.txt` file is sitting next to the audio. Read it with the `Read` tool like any other file.
205
278
 
206
- If transcription fails (e.g. file >25MB, Whisper API's own hard limit), leave the audio in place and skip the `.txt`. The human can ask you to split/compress later.
279
+ If Whisper fails (file >25MB is Whisper's own hard cap; rate-limit; network), leave the audio in place and skip the `.txt`. The human can ask you to split/compress later.
207
280
 
208
281
  ### Pretty filenames (optional)
209
282
 
210
- Tell the human you can keep the raw `<FILE_ID>.mp3` filenames, OR you can also rename to something human-readable. If they want pretty names:
283
+ Tell the human you can keep raw `<id>.mp3` filenames or also create human-readable copies. If they want pretty names:
211
284
 
212
285
  ```bash
213
- # After successful transcribe, also write a symlink or copy with a nicer name:
214
286
  NICE="$(date -d "<start_time>" +%Y-%m-%d_%H%M)_<sanitised filename>"
215
287
  ln -s "<FILE_ID>.mp3" "workspace/files/audio/plaud/${NICE}.mp3"
216
288
  ln -s "<FILE_ID>.mp3.txt" "workspace/files/audio/plaud/${NICE}.txt"
@@ -218,79 +290,70 @@ ln -s "<FILE_ID>.mp3.txt" "workspace/files/audio/plaud/${NICE}.txt"
218
290
 
219
291
  (Sanitise `filename` by stripping `/\\:*?"<>|`.)
220
292
 
221
- Don't rename the originals — keep `<id>.mp3` as the canonical name so dedup keeps working.
293
+ Don't rename originals — `<id>.mp3` stays canonical so dedup keeps working.
222
294
 
223
295
  ---
224
296
 
225
297
  ## Cadence — CRON or PULSE?
226
298
 
227
- **There is no automatic cron set up by this skill.** You and your human decide together. Two reasonable patterns:
299
+ **This skill installs no automatic schedule.** You and your human decide together.
228
300
 
229
301
  ### Pattern A — CRON every N minutes
230
302
 
231
- When the human wants near-real-time freshness ("any time I record something, you should know about it within 15 minutes"), add an entry to `workspace/CRONS.json`:
303
+ When the human wants near-real-time freshness, add an entry to `workspace/CRONS.json`:
232
304
 
233
305
  ```json
234
306
  {
235
307
  "id": "plaud-sync",
236
308
  "schedule": "*/15 * * * *",
237
- "task": "Run a Plaud sync: list new recordings, download any new ones into workspace/files/audio/plaud/, and transcribe them via /api/whisper/transcribe-file. After, summarise to the human in chat IF there were new recordings — otherwise stay silent.",
309
+ "task": "Run a Plaud sync per the plaud skill: refresh WT if needed, list new recordings, download into workspace/files/audio/plaud/, and transcribe via /api/whisper/transcribe-file. If new recordings were found, summarise to the human in chat. If nothing new, stay silent.",
238
310
  "enabled": true,
239
311
  "oneShot": false
240
312
  }
241
313
  ```
242
314
 
243
- Tune `*/15` to taste. `*/5` for aggressive, `0 * * * *` (top of every hour) for quiet.
315
+ Tune `*/15` to taste. `*/5` for aggressive, `0 * * * *` for quiet.
244
316
 
245
317
  ### Pattern B — PULSE memo
246
318
 
247
- When the human prefers their bloby just *check* during normal pulse wake-ups, add one line to your `MYSELF.md` or `MEMORY.md`:
319
+ When the human prefers their bloby just *check* during normal pulse wake-ups, add one line to `MYSELF.md` or `MEMORY.md`:
248
320
 
249
321
  ```
250
322
  - Each pulse, briefly check Plaud for new recordings via the plaud skill. If there's something new, transcribe and decide whether to surface it. If nothing new, move on silently.
251
323
  ```
252
324
 
253
- Pulse runs every 30 min by default. No CRON entry needed. Less aggressive than Pattern A, fits naturally with whatever else you're doing at pulse time.
325
+ Pulse runs every 30 min by default.
254
326
 
255
327
  ### Or: don't auto-sync at all
256
328
 
257
- Some humans only want manual control: *"Bloby, pull anything new from Plaud."* That's also fine — just keep the skill installed, no CRON, no pulse memo, you sync when asked.
329
+ Manual only. Keep the skill installed, no CRON, no pulse memo, sync when asked.
258
330
 
259
- **Always check with the human first.** Default to Pattern B for new installs unless they tell you otherwise.
331
+ **Default to Pattern B for new installs unless the human says otherwise.**
260
332
 
261
333
  ---
262
334
 
263
335
  ## Re-auth (401 handling)
264
336
 
265
- When any Plaud call returns 401:
337
+ Two different 401s, two different fixes.
338
+
339
+ | Endpoint that 401'd | What expired | Fix |
340
+ |---|---|---|
341
+ | `/file/simple/web`, `/file/temp-url/*`, `/device/list` (auth: WT) | Workspace token expired | Re-mint a WT from the cached UT (Step 7b). Don't bother the human. |
342
+ | `/user-app/auth/workspace/token/...`, `/team-app/workspaces/list`, `/user/me` (auth: UT) | User token expired | Tell the human, re-run OTP from Step 1. |
266
343
 
267
- 1. Tell the human in chat: *"Your Plaud connection expired. Want me to re-pair?"* Don't silently fail.
268
- 2. If they say yes, re-run the OTP flow from Step 1. Overwrite the `plaud_token` setting.
269
- 3. If they signed up with Google/Apple originally, prompt for the paste-token fallback instead.
270
- 4. Don't keep retrying with the dead token — pause the sync until re-paired.
344
+ If you can't tell which token expired (e.g. you tried to mint a WT and got 401), assume UT is dead → re-OTP.
271
345
 
272
346
  ---
273
347
 
274
348
  ## Disconnect
275
349
 
350
+ Delete the state file:
351
+
276
352
  ```bash
277
- curl -s -X POST 'http://localhost:7400/api/settings' \
278
- -H 'Content-Type: application/json' \
279
- -d '{"key":"plaud_token","value":""}'
280
- curl -s -X POST 'http://localhost:7400/api/settings' \
281
- -H 'Content-Type: application/json' \
282
- -d '{"key":"plaud_api_base","value":""}'
353
+ rm -f workspace/.plaud.json
283
354
  ```
284
355
 
285
- Recordings already on disk stay. The user can also disable the CRON entry (or remove it from `CRONS.json`).
286
-
287
- ---
288
-
289
- ## Workspaces (advanced)
290
-
291
- Plaud's "workspace" is their multi-account team feature. Personal accounts don't usually need to worry about this — the API responds correctly without a workspace token. If a human ever reports recordings missing that they can see in the Plaud app, it's likely a workspace-scoped recording.
292
-
293
- To resolve a workspace token: there's an undocumented `/workspace/...` endpoint that mints a workspace-scoped token. OpenPlaud's `src/lib/plaud/workspace.ts` is the reference if you ever need it. Don't bother unless the human hits this case.
356
+ Recordings already on disk stay. The human can also disable the CRON entry / remove it from `CRONS.json`.
294
357
 
295
358
  ---
296
359
 
@@ -299,27 +362,32 @@ To resolve a workspace token: there's an undocumented `/workspace/...` endpoint
299
362
  - **No Plaud transcription.** We transcribe ourselves with Whisper. Plaud's own AI subscription is bypassed entirely.
300
363
  - **No dashboard.** OpenPlaud has a slick UI for browsing recordings. We don't. The bloby's job is to *read* the transcripts and act on them — summaries, action items, emails — using the normal workspace tools. If the human wants a UI, build one into `workspace/client/` as a normal workspace app.
301
364
  - **No push from Plaud.** No webhooks exist. You only know about new recordings when you ask.
302
- - **No editing recordings.** The Plaud API technically supports `PATCH /file/<id>` to rename. We don't expose it here — keep canonical `<id>.mp3` names.
303
- - **No real-time streaming.** Plaud syncs to its cloud *after* the recording finishes. Expect a lag of seconds-to-minutes between "user stopped recording" and "file appears in `/file/simple/web`."
365
+ - **No editing recordings.** The Plaud API technically supports `PATCH /file/<id>` to rename. We don't expose it — keep canonical `<id>.mp3` names.
366
+ - **No real-time streaming.** Plaud syncs to its cloud *after* the recording finishes. Expect seconds-to-minutes of lag between "user stopped recording" and "file appears in `/file/simple/web`."
304
367
 
305
368
  ---
306
369
 
307
370
  ## Quick Reference
308
371
 
309
- | Action | curl |
310
- |---|---|
311
- | Send OTP | `POST <base>/auth/otp-send-code` body `{username}` |
312
- | Verify OTP | `POST <base>/auth/otp-login` body `{code, token}` |
313
- | List devices | `GET <base>/device/list` |
314
- | List recordings | `GET <base>/file/simple/web?skip=0&limit=50&is_trash=0&sort_by=edit_time&is_desc=true` |
315
- | Get download URL | `GET <base>/file/temp-url/<id>?is_opus=0` |
316
- | Transcribe local file | `POST http://localhost:7400/api/whisper/transcribe-file` body `{path, saveTranscriptNext}` |
317
- | Save setting | `POST http://localhost:7400/api/settings` body `{key, value}` |
372
+ | Action | curl | Auth |
373
+ |---|---|---|
374
+ | Send OTP | `POST <base>/auth/otp-send-code` body `{username}` | none |
375
+ | Verify OTP → UT | `POST <base>/auth/otp-login` body `{code, token}` | none |
376
+ | Profile | `GET <base>/user/me` | UT |
377
+ | List workspaces | `GET <base>/team-app/workspaces/list?need_personal_workspace=true` | UT |
378
+ | Mint WT | `POST <base>/user-app/auth/workspace/token/<workspaceId>` body `{}` | UT |
379
+ | List devices | `GET <base>/device/list` | **WT** |
380
+ | List recordings | `GET <base>/file/simple/web?skip=0&limit=50&is_trash=0&sort_by=edit_time&is_desc=true` | **WT** |
381
+ | Get download URL | `GET <base>/file/temp-url/<id>?is_opus=0` | **WT** |
382
+ | Download audio | `GET <temp_url>` | none (signed) |
383
+ | Transcribe local file | `POST http://localhost:7400/api/whisper/transcribe-file` body `{path, saveTranscriptNext}` | none (exempt) |
384
+
385
+ State file: `workspace/.plaud.json` — read/write with `Read` / `Write`. **No `/api/settings` calls** — that endpoint requires a portal Bearer token the skill can't easily produce.
318
386
 
319
- All Plaud requests need `Authorization: Bearer <JWT>` + a browser-style `User-Agent`.
387
+ All Plaud requests need a browser-style `User-Agent`.
320
388
 
321
389
  ---
322
390
 
323
391
  ## Credit
324
392
 
325
- Plaud API shape is the same one [OpenPlaud](https://github.com/openplaud/openplaud) uses — they did the reverse-engineering work. This skill reimplements just the parts a bloby needs.
393
+ Plaud API shape is the same one [OpenPlaud](https://github.com/openplaud/openplaud) uses — they did the reverse-engineering work, including the painful workspace-token discovery (their issue #66). This skill reimplements just the parts a bloby needs.