@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.
@@ -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-xpeRZhsZ.js"></script>
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
- // Check active WiFi connection
111
- const { stdout } = await runExec("nmcli", ["-t", "-f", "IN-USE,SSID,SIGNAL,SECURITY", "dev", "wifi", "list"], { timeoutMs: 10_000 });
112
- const networks = parseWifiList(stdout);
113
- const active = networks.find((n) => n.active);
114
- const ip = active ? await getWifiIp() : null;
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: !!active,
118
- ssid: active?.ssid ?? null,
119
- signal: active?.signal ?? null,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.102",
3
+ "version": "1.0.104",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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 assistant's knowledge — everything it knows about your business. Files are organised into folders:
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
- All files are markdown (`.md`) plain text with simple formatting. You can upload new files by dragging them onto the Files page.
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
- **Disconnecting:** Tap **Disconnect** to drop the current WiFi connection. This is useful if you want to switch back to Ethernet or connect to a different network.
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
- **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.
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
- **Rescanning:** While the network list is open, tap **Scan** again at the top to refresh the list with the latest networks.
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?