@rubytech/taskmaster 1.0.102 → 1.0.104
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/dist/build-info.json +3 -3
- package/dist/control-ui/assets/{index-xpeRZhsZ.js → index-BVS_Pain.js} +80 -57
- package/dist/control-ui/assets/index-BVS_Pain.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/gateway/server-methods/wifi.js +207 -10
- package/package.json +1 -1
- package/taskmaster-docs/USER-GUIDE.md +31 -9
- package/templates/maxy/memory/public/FAQ.md +42 -0
- package/dist/control-ui/assets/index-xpeRZhsZ.js.map +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Taskmaster Control</title>
|
|
7
7
|
<meta name="color-scheme" content="dark light" />
|
|
8
8
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-BVS_Pain.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-DjhCZlZd.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
* RPC handlers for WiFi network management on Linux (Raspberry Pi).
|
|
3
3
|
* Uses nmcli (NetworkManager CLI) which ships with Raspberry Pi OS Bookworm.
|
|
4
4
|
* All methods require operator.admin scope (enforced by the catch-all in server-methods.ts).
|
|
5
|
+
*
|
|
6
|
+
* Connection lifecycle:
|
|
7
|
+
* - wifi.connect → creates/updates NM profile, enables autoconnect, disables power save
|
|
8
|
+
* - wifi.disconnect → disables autoconnect, brings connection down (intentional)
|
|
9
|
+
* - wifi.reconnect → re-enables autoconnect, brings saved profile up (no password needed)
|
|
10
|
+
* - wifi.forget → deletes the NM connection profile entirely
|
|
11
|
+
*
|
|
12
|
+
* NetworkManager handles auto-reconnect natively when autoconnect=yes.
|
|
13
|
+
* We additionally disable WiFi power save to prevent the wlan0 adapter from sleeping.
|
|
5
14
|
*/
|
|
6
15
|
import os from "node:os";
|
|
7
16
|
import { runExec } from "../../process/exec.js";
|
|
@@ -86,12 +95,102 @@ async function getWifiIp() {
|
|
|
86
95
|
}
|
|
87
96
|
return null;
|
|
88
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Query wlan0 device state directly via `nmcli dev show wlan0`.
|
|
100
|
+
* This is more reliable than checking IN-USE in scan results, which can be stale.
|
|
101
|
+
*/
|
|
102
|
+
async function getWlan0State() {
|
|
103
|
+
try {
|
|
104
|
+
const { stdout } = await runExec("nmcli", ["-t", "-f", "GENERAL.STATE,GENERAL.CONNECTION", "dev", "show", "wlan0"], { timeoutMs: 5_000 });
|
|
105
|
+
// Output: GENERAL.STATE:100 (connected)\nGENERAL.CONNECTION:MyWiFi
|
|
106
|
+
let connected = false;
|
|
107
|
+
let ssid = null;
|
|
108
|
+
for (const line of stdout.split("\n")) {
|
|
109
|
+
if (line.startsWith("GENERAL.STATE:")) {
|
|
110
|
+
// State 100 = connected, anything else = disconnected
|
|
111
|
+
connected = line.includes("100");
|
|
112
|
+
}
|
|
113
|
+
if (line.startsWith("GENERAL.CONNECTION:")) {
|
|
114
|
+
const val = line.slice("GENERAL.CONNECTION:".length).trim();
|
|
115
|
+
if (val && val !== "--")
|
|
116
|
+
ssid = val;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { connected, ssid };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { connected: false, ssid: null };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Find a saved WiFi connection profile managed by NetworkManager.
|
|
127
|
+
* Returns { name, ssid, autoconnect } or null if none exists.
|
|
128
|
+
*/
|
|
129
|
+
async function getSavedWifiConnection() {
|
|
130
|
+
try {
|
|
131
|
+
// List all connection profiles: NAME:TYPE:AUTOCONNECT
|
|
132
|
+
const { stdout } = await runExec("nmcli", ["-t", "-f", "NAME,TYPE,AUTOCONNECT", "con", "show"], { timeoutMs: 5_000 });
|
|
133
|
+
for (const line of stdout.split("\n")) {
|
|
134
|
+
if (!line.trim())
|
|
135
|
+
continue;
|
|
136
|
+
const placeholder = "\x00";
|
|
137
|
+
const safe = line.replace(/\\:/g, placeholder);
|
|
138
|
+
const parts = safe.split(":");
|
|
139
|
+
if (parts.length < 3)
|
|
140
|
+
continue;
|
|
141
|
+
const name = parts[0].replace(new RegExp(placeholder, "g"), ":").trim();
|
|
142
|
+
const type = parts[1].trim();
|
|
143
|
+
const autoconnect = parts[2].trim().toLowerCase() === "yes";
|
|
144
|
+
// 802-11-wireless is the nmcli type for WiFi connections
|
|
145
|
+
if (type === "802-11-wireless") {
|
|
146
|
+
return { name, ssid: name, autoconnect };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Non-critical
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Disable WiFi power save on wlan0 to prevent the adapter from sleeping.
|
|
157
|
+
* This is a common cause of WiFi drops on Raspberry Pi.
|
|
158
|
+
*/
|
|
159
|
+
async function disableWifiPowerSave(log) {
|
|
160
|
+
try {
|
|
161
|
+
await runExec("iw", ["dev", "wlan0", "set", "power_save", "off"], { timeoutMs: 5_000 });
|
|
162
|
+
log.info?.("WiFi power save disabled on wlan0");
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
// iw might not be installed or wlan0 might not exist — non-critical
|
|
166
|
+
log.warn(`Could not disable WiFi power save: ${err instanceof Error ? err.message : String(err)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Ensure a connection profile has autoconnect enabled and high priority.
|
|
171
|
+
*/
|
|
172
|
+
async function ensureAutoconnect(connectionName) {
|
|
173
|
+
try {
|
|
174
|
+
await runExec("nmcli", [
|
|
175
|
+
"con",
|
|
176
|
+
"modify",
|
|
177
|
+
connectionName,
|
|
178
|
+
"connection.autoconnect",
|
|
179
|
+
"yes",
|
|
180
|
+
"connection.autoconnect-priority",
|
|
181
|
+
"100",
|
|
182
|
+
], { timeoutMs: 5_000 });
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Non-critical — profile might not exist yet
|
|
186
|
+
}
|
|
187
|
+
}
|
|
89
188
|
// ---------------------------------------------------------------------------
|
|
90
189
|
// Handlers
|
|
91
190
|
// ---------------------------------------------------------------------------
|
|
92
191
|
export const wifiHandlers = {
|
|
93
192
|
/**
|
|
94
|
-
* Return current WiFi connection status.
|
|
193
|
+
* Return current WiFi connection status, including saved profile info.
|
|
95
194
|
*/
|
|
96
195
|
"wifi.status": async ({ respond, context }) => {
|
|
97
196
|
if (!requireLinux(respond))
|
|
@@ -104,20 +203,38 @@ export const wifiHandlers = {
|
|
|
104
203
|
ssid: null,
|
|
105
204
|
signal: null,
|
|
106
205
|
ip: null,
|
|
206
|
+
savedSsid: null,
|
|
207
|
+
autoconnect: false,
|
|
107
208
|
});
|
|
108
209
|
return;
|
|
109
210
|
}
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
211
|
+
// Query wlan0 device state directly — more reliable than scan results
|
|
212
|
+
// which can be stale right after connecting.
|
|
213
|
+
const deviceState = await getWlan0State();
|
|
214
|
+
const ip = deviceState.connected ? await getWifiIp() : null;
|
|
215
|
+
// Get signal strength from scan results (non-blocking, may be stale)
|
|
216
|
+
let signal = null;
|
|
217
|
+
if (deviceState.connected && deviceState.ssid) {
|
|
218
|
+
try {
|
|
219
|
+
const { stdout } = await runExec("nmcli", ["-t", "-f", "IN-USE,SSID,SIGNAL,SECURITY", "dev", "wifi", "list"], { timeoutMs: 5_000 });
|
|
220
|
+
const networks = parseWifiList(stdout);
|
|
221
|
+
const active = networks.find((n) => n.active);
|
|
222
|
+
signal = active?.signal ?? null;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Non-critical — signal strength is cosmetic
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Check for saved connection profile
|
|
229
|
+
const saved = await getSavedWifiConnection();
|
|
115
230
|
respond(true, {
|
|
116
231
|
available: true,
|
|
117
|
-
connected:
|
|
118
|
-
ssid:
|
|
119
|
-
signal
|
|
232
|
+
connected: deviceState.connected,
|
|
233
|
+
ssid: deviceState.ssid,
|
|
234
|
+
signal,
|
|
120
235
|
ip,
|
|
236
|
+
savedSsid: saved?.ssid ?? null,
|
|
237
|
+
autoconnect: saved?.autoconnect ?? false,
|
|
121
238
|
});
|
|
122
239
|
}
|
|
123
240
|
catch (err) {
|
|
@@ -147,6 +264,7 @@ export const wifiHandlers = {
|
|
|
147
264
|
},
|
|
148
265
|
/**
|
|
149
266
|
* Connect to a WiFi network.
|
|
267
|
+
* Creates a persistent NM profile with autoconnect enabled and disables power save.
|
|
150
268
|
* Params: { ssid: string, password?: string }
|
|
151
269
|
*/
|
|
152
270
|
"wifi.connect": async ({ params, respond, context }) => {
|
|
@@ -172,12 +290,19 @@ export const wifiHandlers = {
|
|
|
172
290
|
const combined = `${stdout}\n${stderr}`;
|
|
173
291
|
const success = combined.includes("successfully activated") || combined.includes("successfully added");
|
|
174
292
|
if (success) {
|
|
293
|
+
// Ensure autoconnect is enabled and high priority for reliable reconnection
|
|
294
|
+
await ensureAutoconnect(ssid);
|
|
295
|
+
// Disable WiFi power save to prevent adapter sleep
|
|
296
|
+
await disableWifiPowerSave(context.logGateway);
|
|
175
297
|
const ip = await getWifiIp();
|
|
176
298
|
respond(true, { connected: true, ssid, ip });
|
|
177
299
|
}
|
|
178
300
|
else {
|
|
179
301
|
// nmcli returned 0 but no success message — treat as unexpected
|
|
180
302
|
context.logGateway.warn(`wifi.connect: unexpected output: ${combined.slice(0, 300)}`);
|
|
303
|
+
// Still try to configure autoconnect and power save
|
|
304
|
+
await ensureAutoconnect(ssid);
|
|
305
|
+
await disableWifiPowerSave(context.logGateway);
|
|
181
306
|
respond(true, { connected: true, ssid, ip: null });
|
|
182
307
|
}
|
|
183
308
|
}
|
|
@@ -200,7 +325,9 @@ export const wifiHandlers = {
|
|
|
200
325
|
}
|
|
201
326
|
},
|
|
202
327
|
/**
|
|
203
|
-
* Disconnect from the current WiFi network.
|
|
328
|
+
* Disconnect from the current WiFi network (intentional).
|
|
329
|
+
* Disables autoconnect so NetworkManager won't automatically reconnect.
|
|
330
|
+
* The saved profile is preserved — use wifi.forget to delete it.
|
|
204
331
|
*/
|
|
205
332
|
"wifi.disconnect": async ({ respond, context }) => {
|
|
206
333
|
if (!requireLinux(respond))
|
|
@@ -225,6 +352,13 @@ export const wifiHandlers = {
|
|
|
225
352
|
respond(true, { disconnected: true }); // Already disconnected
|
|
226
353
|
return;
|
|
227
354
|
}
|
|
355
|
+
// Disable autoconnect so NM won't immediately reconnect
|
|
356
|
+
try {
|
|
357
|
+
await runExec("nmcli", ["con", "modify", connectionName, "connection.autoconnect", "no"], { timeoutMs: 5_000 });
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Non-critical — proceed with disconnect anyway
|
|
361
|
+
}
|
|
228
362
|
await runExec("nmcli", ["con", "down", connectionName], { timeoutMs: 10_000 });
|
|
229
363
|
respond(true, { disconnected: true });
|
|
230
364
|
}
|
|
@@ -233,4 +367,67 @@ export const wifiHandlers = {
|
|
|
233
367
|
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "Failed to disconnect from WiFi"));
|
|
234
368
|
}
|
|
235
369
|
},
|
|
370
|
+
/**
|
|
371
|
+
* Reconnect to a saved WiFi profile (no password needed).
|
|
372
|
+
* Re-enables autoconnect and brings the saved connection up.
|
|
373
|
+
*/
|
|
374
|
+
"wifi.reconnect": async ({ respond, context }) => {
|
|
375
|
+
if (!requireLinux(respond))
|
|
376
|
+
return;
|
|
377
|
+
try {
|
|
378
|
+
if (!(await nmcliAvailable())) {
|
|
379
|
+
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "NetworkManager (nmcli) is not installed"));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const saved = await getSavedWifiConnection();
|
|
383
|
+
if (!saved) {
|
|
384
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "No saved WiFi connection to reconnect"));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
// Re-enable autoconnect
|
|
388
|
+
await ensureAutoconnect(saved.name);
|
|
389
|
+
// Bring the connection up
|
|
390
|
+
await runExec("nmcli", ["con", "up", saved.name], { timeoutMs: 30_000 });
|
|
391
|
+
// Disable power save after reconnecting
|
|
392
|
+
await disableWifiPowerSave(context.logGateway);
|
|
393
|
+
const ip = await getWifiIp();
|
|
394
|
+
respond(true, { connected: true, ssid: saved.ssid, ip });
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
const errObj = err;
|
|
398
|
+
const detail = errObj.stderr?.trim() || errObj.message || "Reconnect failed";
|
|
399
|
+
context.logGateway.warn(`wifi.reconnect failed: ${detail}`);
|
|
400
|
+
let message = "Failed to reconnect to WiFi";
|
|
401
|
+
if (detail.includes("No network with SSID") || detail.includes("not available")) {
|
|
402
|
+
message = "Network not in range — try moving closer to the router";
|
|
403
|
+
}
|
|
404
|
+
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, message));
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
/**
|
|
408
|
+
* Forget a saved WiFi network — deletes the NM connection profile entirely.
|
|
409
|
+
* After this, the user must re-enter the password to connect again.
|
|
410
|
+
*/
|
|
411
|
+
"wifi.forget": async ({ respond, context }) => {
|
|
412
|
+
if (!requireLinux(respond))
|
|
413
|
+
return;
|
|
414
|
+
try {
|
|
415
|
+
if (!(await nmcliAvailable())) {
|
|
416
|
+
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "NetworkManager (nmcli) is not installed"));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const saved = await getSavedWifiConnection();
|
|
420
|
+
if (!saved) {
|
|
421
|
+
respond(true, { forgotten: true }); // Nothing to forget
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// Delete the connection profile — this also disconnects if active
|
|
425
|
+
await runExec("nmcli", ["con", "delete", saved.name], { timeoutMs: 10_000 });
|
|
426
|
+
respond(true, { forgotten: true, ssid: saved.ssid });
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
context.logGateway.warn(`wifi.forget failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
430
|
+
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "Failed to forget WiFi network"));
|
|
431
|
+
}
|
|
432
|
+
},
|
|
236
433
|
};
|
package/package.json
CHANGED
|
@@ -539,16 +539,34 @@ The free tier works for low-volume use (a handful of voice notes or images per d
|
|
|
539
539
|
|
|
540
540
|
## Your Files
|
|
541
541
|
|
|
542
|
-
The **Files** page shows your
|
|
542
|
+
The **Files** page shows your full workspace — everything your assistant knows and how it's configured. The top-level folders are:
|
|
543
|
+
|
|
544
|
+
| Folder | What it contains |
|
|
545
|
+
|--------|-----------------|
|
|
546
|
+
| **agents/** | Your assistant's identity and personality files (admin and public agents) |
|
|
547
|
+
| **memory/** | Knowledge files organised by audience (see below) |
|
|
548
|
+
| **skills/** | Skill packs that teach your assistant new capabilities |
|
|
549
|
+
|
|
550
|
+
Inside **memory/**, files are organised by who can see them:
|
|
543
551
|
|
|
544
552
|
| Folder | Who can see it | What it's for |
|
|
545
553
|
|--------|---------------|---------------|
|
|
546
|
-
| **public/** | Both assistants | Information your customers can ask about — services, pricing, opening hours |
|
|
547
|
-
| **shared/** | Both assistants | Internal guidance — how to handle enquiries, tone of voice, escalation rules |
|
|
548
|
-
| **admin/** | Admin assistant only | Private notes, business plans, personal reminders — customers never see this |
|
|
549
|
-
| **users/** | One customer at a time | A folder for each customer (by phone number) with their profile, preferences, and history |
|
|
554
|
+
| **memory/public/** | Both assistants | Information your customers can ask about — services, pricing, opening hours |
|
|
555
|
+
| **memory/shared/** | Both assistants | Internal guidance — how to handle enquiries, tone of voice, escalation rules |
|
|
556
|
+
| **memory/admin/** | Admin assistant only | Private notes, business plans, personal reminders — customers never see this |
|
|
557
|
+
| **memory/users/** | One customer at a time | A folder for each customer (by phone number) with their profile, preferences, and history |
|
|
558
|
+
|
|
559
|
+
All files are markdown (`.md`) — plain text with simple formatting.
|
|
560
|
+
|
|
561
|
+
### Uploading and Managing Files
|
|
562
|
+
|
|
563
|
+
Click any **folder** in the tree to set it as your working directory. The folder highlights with a left border to show it's selected. The **Upload** and **New Folder** buttons show where files will go — for example, "Upload to memory/public".
|
|
550
564
|
|
|
551
|
-
|
|
565
|
+
- **Upload** — Click the Upload button or drag `.md` files onto a folder in the tree
|
|
566
|
+
- **New Folder** — Creates a new folder inside the current directory (useful for adding skill packs or organising memory)
|
|
567
|
+
- **Delete** — Select a file and click Delete. The button changes to "Confirm delete?" — click again within 3 seconds to confirm. Empty folders show a small × on hover to remove them
|
|
568
|
+
- **Move** — Drag any file onto a different folder to move it there
|
|
569
|
+
- **Multi-select** — Hold Ctrl (or Cmd on Mac) and click multiple files, then use Download to save them all
|
|
552
570
|
|
|
553
571
|
When you add or change a file, your assistant picks it up automatically — no restart needed. The status light on the Files page turns red when files have changed since the last index, so you can see at a glance whether a re-index is needed.
|
|
554
572
|
|
|
@@ -666,11 +684,15 @@ If your Pi is connected by Ethernet cable, you can switch to WiFi directly from
|
|
|
666
684
|
|
|
667
685
|
**Signal bars** next to each network name show the signal strength — more lit bars means a stronger signal.
|
|
668
686
|
|
|
669
|
-
**
|
|
687
|
+
**Auto-reconnect:** Once connected, your Pi remembers the WiFi network and password. If the connection drops (e.g. router reboot, temporary signal loss), the Pi automatically reconnects without any action needed. The WiFi row shows "Saved: (network name)" when a saved network exists but the connection is temporarily down.
|
|
670
688
|
|
|
671
|
-
**
|
|
689
|
+
**Reconnecting:** If WiFi was manually disconnected and you want to reconnect to the saved network, tap **Reconnect** — no need to re-enter the password.
|
|
690
|
+
|
|
691
|
+
**Disconnecting:** Tap **Disconnect** to intentionally drop the current WiFi connection. This prevents auto-reconnect until you tap **Reconnect** or connect to a new network.
|
|
672
692
|
|
|
673
|
-
**
|
|
693
|
+
**Forgetting a network:** Tap **Forget** to remove the saved WiFi password entirely. After forgetting, you'll need to scan and enter the password again to reconnect.
|
|
694
|
+
|
|
695
|
+
**Closing the list:** Tap **Close** to dismiss the network list without making changes. You can also tap the network you're already connected to (marked with a green tick) to dismiss the list.
|
|
674
696
|
|
|
675
697
|
> **Tip:** Connect an Ethernet cable first, open the Control Panel, then use the WiFi row to connect. Once WiFi is working, you can remove the Ethernet cable.
|
|
676
698
|
|
|
@@ -80,6 +80,9 @@ Optional add-ons that teach Maxy new domains. £5 each or all for £20. One-time
|
|
|
80
80
|
- **Elderly Care** — medication management, safety check-ins, carer communication, emergency info
|
|
81
81
|
- **Social** — birthdays, gift planning, event organisation, friend/family context, group coordination
|
|
82
82
|
|
|
83
|
+
**Premium skills:**
|
|
84
|
+
- **Beagle** — AI booking agent for local services. Find, quote, and book tradespeople and other professionals. Pay-per-booking (10% fee, capped at £50). See beagle.bot
|
|
85
|
+
|
|
83
86
|
### Can I buy skill packs later?
|
|
84
87
|
|
|
85
88
|
Yes. Start with what you need and add more any time. They're permanent — no ongoing cost.
|
|
@@ -90,6 +93,45 @@ Any time. No minimum term. Your data stays on your device — Maxy never had acc
|
|
|
90
93
|
|
|
91
94
|
---
|
|
92
95
|
|
|
96
|
+
## Beagle — Premium Skill
|
|
97
|
+
|
|
98
|
+
### What is Beagle?
|
|
99
|
+
|
|
100
|
+
Beagle is Maxy's AI booking agent for local services. Instead of calling around for quotes, tell Maxy what you need — a plumber, cleaner, electrician, painter — and Beagle handles the rest. It contacts local professionals, compares quotes, and books the best one in. The whole process takes under 4 hours and happens through conversation.
|
|
101
|
+
|
|
102
|
+
Website: beagle.bot
|
|
103
|
+
|
|
104
|
+
### How does Beagle work with Maxy?
|
|
105
|
+
|
|
106
|
+
Beagle extends Maxy's Home skill pack. Where the Home pack remembers your tradespeople and reminds you about maintenance, Beagle actively finds and books new professionals when you need them. Ask Maxy to fix something, and it handles the sourcing, quoting, and booking.
|
|
107
|
+
|
|
108
|
+
### What does Beagle cost?
|
|
109
|
+
|
|
110
|
+
Beagle uses a pay-per-booking model, not a flat skill pack price:
|
|
111
|
+
- 10% booking fee on the job total (minimum £2, maximum £50)
|
|
112
|
+
- Nothing upfront — the fee is collected after the job is done
|
|
113
|
+
- The provider deducts the fee from what you owe — your total stays the same
|
|
114
|
+
|
|
115
|
+
Example: A £130 plumbing job → £13 Beagle fee. The plumber charges you £117. You pay £130 total, same as booking direct.
|
|
116
|
+
|
|
117
|
+
### What if Beagle can't find someone?
|
|
118
|
+
|
|
119
|
+
You pay nothing. If Beagle can't source a suitable quote, the request is closed and there's no charge.
|
|
120
|
+
|
|
121
|
+
### What services can Beagle book?
|
|
122
|
+
|
|
123
|
+
Starting with tradespeople — plumbers, electricians, painters, decorators, handymen, gardeners, cleaners. Expanding to any bookable service: hairdressers, personal trainers, accountants, removals, restaurant tables, wedding photographers.
|
|
124
|
+
|
|
125
|
+
### What areas does Beagle cover?
|
|
126
|
+
|
|
127
|
+
UK-wide.
|
|
128
|
+
|
|
129
|
+
### Is Beagle different from Bark or Checkatrade?
|
|
130
|
+
|
|
131
|
+
Yes. Those platforms sell your details to multiple tradespeople who compete to contact you — you get bombarded with calls. Beagle does the opposite: it contacts providers on your behalf, compares quotes behind the scenes, and gives you one confirmed booking. No spam, no runaround.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
93
135
|
## Setup
|
|
94
136
|
|
|
95
137
|
### How long does setup take?
|