@rubytech/taskmaster 1.0.65 → 1.0.67
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/agents/memory-search.js +1 -1
- package/dist/build-info.json +2 -2
- package/dist/config/schema.js +1 -1
- package/dist/config/zod-schema.agent-runtime.js +1 -1
- package/dist/control-ui/assets/{index-o5Xs9S4u.js → index-8pJBjxcK.js} +443 -397
- package/dist/control-ui/assets/index-8pJBjxcK.js.map +1 -0
- package/dist/control-ui/assets/{index-DmifehTc.css → index-Uo_tQYx1.css} +1 -1
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/control-ui.js +11 -3
- package/dist/gateway/net.js +29 -0
- package/dist/gateway/server-methods/memory.js +1 -0
- package/dist/memory/embeddings.js +140 -31
- package/dist/memory/manager.js +1 -0
- package/package.json +1 -1
- package/taskmaster-docs/USER-GUIDE.md +53 -0
- package/dist/control-ui/assets/index-o5Xs9S4u.js.map +0 -1
package/dist/gateway/net.js
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js";
|
|
3
|
+
/**
|
|
4
|
+
* Check if an IPv4 address belongs to a private (RFC 1918) or link-local range.
|
|
5
|
+
* These addresses are non-routable on the public internet, so any request
|
|
6
|
+
* from one is by definition on the local network — safe for LAN access.
|
|
7
|
+
*
|
|
8
|
+
* Ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16
|
|
9
|
+
*/
|
|
10
|
+
function isPrivateIPv4(ip) {
|
|
11
|
+
const parts = ip.split(".");
|
|
12
|
+
if (parts.length !== 4)
|
|
13
|
+
return false;
|
|
14
|
+
const a = parseInt(parts[0], 10);
|
|
15
|
+
const b = parseInt(parts[1], 10);
|
|
16
|
+
if (Number.isNaN(a) || Number.isNaN(b))
|
|
17
|
+
return false;
|
|
18
|
+
if (a === 10)
|
|
19
|
+
return true; // 10.0.0.0/8
|
|
20
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
21
|
+
return true; // 172.16.0.0/12
|
|
22
|
+
if (a === 192 && b === 168)
|
|
23
|
+
return true; // 192.168.0.0/16
|
|
24
|
+
if (a === 169 && b === 254)
|
|
25
|
+
return true; // 169.254.0.0/16 (link-local)
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
3
28
|
export function isLoopbackAddress(ip) {
|
|
4
29
|
if (!ip)
|
|
5
30
|
return false;
|
|
@@ -78,6 +103,10 @@ export function isLocalGatewayAddress(ip) {
|
|
|
78
103
|
const tailnetIPv6 = pickPrimaryTailnetIPv6();
|
|
79
104
|
if (tailnetIPv6 && ip.trim().toLowerCase() === tailnetIPv6.toLowerCase())
|
|
80
105
|
return true;
|
|
106
|
+
// Any RFC 1918 private or link-local address is on the local network.
|
|
107
|
+
// These are non-routable on the public internet, so they're safe.
|
|
108
|
+
if (isPrivateIPv4(normalized))
|
|
109
|
+
return true;
|
|
81
110
|
return false;
|
|
82
111
|
}
|
|
83
112
|
/**
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
2
4
|
import { resolveUserPath } from "../utils.js";
|
|
5
|
+
import { getCustomProviderApiKey } from "../agents/model-auth.js";
|
|
3
6
|
import { createGeminiEmbeddingProvider } from "./embeddings-gemini.js";
|
|
4
7
|
import { createOpenAiEmbeddingProvider } from "./embeddings-openai.js";
|
|
5
8
|
import { importNodeLlamaCpp } from "./node-llama.js";
|
|
6
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Default local embedding model. The 0.6B model is small enough to run on
|
|
11
|
+
* any target hardware (Pi 4GB, Pi 8GB, Mac) without GPU and with minimal
|
|
12
|
+
* RAM overhead (~1-2 GB runtime). Larger models (4B, 8B) can be configured
|
|
13
|
+
* explicitly via `local.modelPath` for users who have tested them on their
|
|
14
|
+
* specific hardware — node-llama-cpp's actual memory footprint varies
|
|
15
|
+
* enormously across GPU vendors and Metal/Vulkan backends.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_LOCAL_MODEL = {
|
|
18
|
+
model: "hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf",
|
|
19
|
+
label: "Qwen3-Embedding-0.6B",
|
|
20
|
+
};
|
|
21
|
+
function selectDefaultLocalModel() {
|
|
22
|
+
return DEFAULT_LOCAL_MODEL;
|
|
23
|
+
}
|
|
7
24
|
function canAutoSelectLocal(options) {
|
|
8
25
|
const modelPath = options.local?.modelPath?.trim();
|
|
9
26
|
if (!modelPath)
|
|
@@ -22,30 +39,85 @@ function isMissingApiKeyError(err) {
|
|
|
22
39
|
const message = formatError(err);
|
|
23
40
|
return message.includes("No API key found for provider");
|
|
24
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Deduplicates concurrent model downloads. When multiple agents resolve the
|
|
44
|
+
* same model path, only one download runs; the rest await the same promise.
|
|
45
|
+
*/
|
|
46
|
+
const inflightDownloads = new Map();
|
|
47
|
+
/**
|
|
48
|
+
* Cache of resolved model paths (model URI → absolute file path on disk).
|
|
49
|
+
* Persists across restarts via a JSON file so we skip `resolveModelFile`
|
|
50
|
+
* (which re-downloads even when the file exists due to a filename mismatch
|
|
51
|
+
* between the expected URI-derived name and the actual hf-prefixed name).
|
|
52
|
+
*/
|
|
53
|
+
const RESOLVED_PATHS_FILE = (() => {
|
|
54
|
+
const homeDir = os.homedir();
|
|
55
|
+
return `${homeDir}/.taskmaster/memory/resolved-model-paths.json`;
|
|
56
|
+
})();
|
|
57
|
+
function readResolvedPaths() {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(fsSync.readFileSync(RESOLVED_PATHS_FILE, "utf8"));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function writeResolvedPath(key, resolvedPath) {
|
|
66
|
+
try {
|
|
67
|
+
const dir = RESOLVED_PATHS_FILE.replace(/\/[^/]+$/, "");
|
|
68
|
+
fsSync.mkdirSync(dir, { recursive: true });
|
|
69
|
+
const existing = readResolvedPaths();
|
|
70
|
+
existing[key] = resolvedPath;
|
|
71
|
+
fsSync.writeFileSync(RESOLVED_PATHS_FILE, JSON.stringify(existing, null, 2));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Non-fatal — next restart will re-resolve
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function resolveModelFileOnce(resolveModelFile, modelPath, modelCacheDir) {
|
|
78
|
+
const key = `${modelPath}::${modelCacheDir ?? ""}`;
|
|
79
|
+
// Check on-disk cache first — avoids re-download on restart
|
|
80
|
+
const cached = readResolvedPaths()[key];
|
|
81
|
+
if (cached && fsSync.existsSync(cached)) {
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
// Deduplicate concurrent calls
|
|
85
|
+
const existing = inflightDownloads.get(key);
|
|
86
|
+
if (existing)
|
|
87
|
+
return existing;
|
|
88
|
+
const promise = resolveModelFile(modelPath, modelCacheDir || undefined)
|
|
89
|
+
.then((resolved) => {
|
|
90
|
+
writeResolvedPath(key, resolved);
|
|
91
|
+
return resolved;
|
|
92
|
+
})
|
|
93
|
+
.finally(() => {
|
|
94
|
+
inflightDownloads.delete(key);
|
|
95
|
+
});
|
|
96
|
+
inflightDownloads.set(key, promise);
|
|
97
|
+
return promise;
|
|
98
|
+
}
|
|
25
99
|
async function createLocalEmbeddingProvider(options) {
|
|
26
|
-
const
|
|
100
|
+
const log = createSubsystemLogger("embeddings");
|
|
101
|
+
const hasExplicitModelPath = Boolean(options.local?.modelPath?.trim());
|
|
102
|
+
let modelPath;
|
|
103
|
+
if (hasExplicitModelPath) {
|
|
104
|
+
modelPath = options.local.modelPath.trim();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const selected = selectDefaultLocalModel();
|
|
108
|
+
modelPath = selected.model;
|
|
109
|
+
log.info(`selected tier ${selected.label} (system RAM: ${(os.totalmem() / (1024 ** 3)).toFixed(1)} GB)`);
|
|
110
|
+
}
|
|
27
111
|
const modelCacheDir = options.local?.modelCacheDir?.trim();
|
|
28
112
|
// Lazy-load node-llama-cpp to keep startup light unless local is enabled.
|
|
29
113
|
const { getLlama, resolveModelFile, LlamaLogLevel } = await importNodeLlamaCpp();
|
|
30
114
|
let llama = null;
|
|
31
115
|
let embeddingModel = null;
|
|
32
116
|
let embeddingContext = null;
|
|
33
|
-
const
|
|
34
|
-
if (!llama) {
|
|
35
|
-
llama = await getLlama({ logLevel: LlamaLogLevel.error });
|
|
36
|
-
}
|
|
37
|
-
if (!embeddingModel) {
|
|
38
|
-
const resolved = await resolveModelFile(modelPath, modelCacheDir || undefined);
|
|
39
|
-
embeddingModel = await llama.loadModel({ modelPath: resolved });
|
|
40
|
-
}
|
|
41
|
-
if (!embeddingContext) {
|
|
42
|
-
embeddingContext = await embeddingModel.createEmbeddingContext();
|
|
43
|
-
}
|
|
44
|
-
return embeddingContext;
|
|
45
|
-
};
|
|
46
|
-
return {
|
|
117
|
+
const provider = {
|
|
47
118
|
id: "local",
|
|
48
119
|
model: modelPath,
|
|
120
|
+
state: "idle",
|
|
49
121
|
embedQuery: async (text) => {
|
|
50
122
|
const ctx = await ensureContext();
|
|
51
123
|
const embedding = await ctx.getEmbeddingFor(text);
|
|
@@ -60,6 +132,31 @@ async function createLocalEmbeddingProvider(options) {
|
|
|
60
132
|
return embeddings;
|
|
61
133
|
},
|
|
62
134
|
};
|
|
135
|
+
const ensureContext = async () => {
|
|
136
|
+
if (!llama) {
|
|
137
|
+
// Force CPU-only: GPU backends (Metal on AMD, Vulkan) allocate enormous
|
|
138
|
+
// memory buffers that dwarf the model itself and can OOM the host.
|
|
139
|
+
// Embedding models are small enough that CPU inference is fast and safe.
|
|
140
|
+
llama = await getLlama({ gpu: false, logLevel: LlamaLogLevel.error });
|
|
141
|
+
}
|
|
142
|
+
if (!embeddingModel) {
|
|
143
|
+
try {
|
|
144
|
+
provider.state = "downloading";
|
|
145
|
+
const resolved = await resolveModelFileOnce(resolveModelFile, modelPath, modelCacheDir);
|
|
146
|
+
embeddingModel = await llama.loadModel({ modelPath: resolved, gpuLayers: 0 });
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
provider.state = "error";
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!embeddingContext) {
|
|
154
|
+
embeddingContext = await embeddingModel.createEmbeddingContext();
|
|
155
|
+
}
|
|
156
|
+
provider.state = "ready";
|
|
157
|
+
return embeddingContext;
|
|
158
|
+
};
|
|
159
|
+
return provider;
|
|
63
160
|
}
|
|
64
161
|
export async function createEmbeddingProvider(options) {
|
|
65
162
|
const requestedProvider = options.provider;
|
|
@@ -78,36 +175,48 @@ export async function createEmbeddingProvider(options) {
|
|
|
78
175
|
};
|
|
79
176
|
const formatPrimaryError = (err, provider) => provider === "local" ? formatLocalSetupError(err) : formatError(err);
|
|
80
177
|
if (requestedProvider === "auto") {
|
|
81
|
-
const
|
|
82
|
-
|
|
178
|
+
const errors = [];
|
|
179
|
+
// 1. If user configured an explicit local model file, try it first.
|
|
83
180
|
if (canAutoSelectLocal(options)) {
|
|
84
181
|
try {
|
|
85
182
|
const local = await createProvider("local");
|
|
86
183
|
return { ...local, requestedProvider };
|
|
87
184
|
}
|
|
88
185
|
catch (err) {
|
|
89
|
-
|
|
186
|
+
errors.push(formatLocalSetupError(err));
|
|
90
187
|
}
|
|
91
188
|
}
|
|
92
|
-
|
|
189
|
+
// 2. Try remote providers — preferred when API keys are available
|
|
190
|
+
// (faster, no RAM overhead, no model download).
|
|
191
|
+
// Only consider providers whose key is in the config (apiKeys section).
|
|
192
|
+
// Environment variables are ignored for auto-selection to avoid using
|
|
193
|
+
// stale dev keys that would fail at runtime.
|
|
194
|
+
const remoteProviders = [
|
|
195
|
+
{ id: "openai", configKey: "openai" },
|
|
196
|
+
{ id: "gemini", configKey: "google" },
|
|
197
|
+
];
|
|
198
|
+
for (const { id, configKey } of remoteProviders) {
|
|
199
|
+
if (!getCustomProviderApiKey(options.config, configKey))
|
|
200
|
+
continue;
|
|
93
201
|
try {
|
|
94
|
-
const result = await createProvider(
|
|
202
|
+
const result = await createProvider(id);
|
|
95
203
|
return { ...result, requestedProvider };
|
|
96
204
|
}
|
|
97
205
|
catch (err) {
|
|
98
|
-
|
|
99
|
-
if (isMissingApiKeyError(err)) {
|
|
100
|
-
missingKeyErrors.push(message);
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
throw new Error(message);
|
|
206
|
+
errors.push(formatPrimaryError(err, id));
|
|
104
207
|
}
|
|
105
208
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
209
|
+
// 3. Fall back to local (downloads default model on first use).
|
|
210
|
+
try {
|
|
211
|
+
const local = await createProvider("local");
|
|
212
|
+
return { ...local, requestedProvider };
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
errors.push(formatLocalSetupError(err));
|
|
109
216
|
}
|
|
110
|
-
throw new Error(
|
|
217
|
+
throw new Error(errors.length > 0
|
|
218
|
+
? errors.join("\n\n")
|
|
219
|
+
: "No embeddings provider available.");
|
|
111
220
|
}
|
|
112
221
|
try {
|
|
113
222
|
const primary = await createProvider(requestedProvider);
|
package/dist/memory/manager.js
CHANGED
package/package.json
CHANGED
|
@@ -533,6 +533,8 @@ All files are markdown (`.md`) — plain text with simple formatting. You can up
|
|
|
533
533
|
|
|
534
534
|
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.
|
|
535
535
|
|
|
536
|
+
After a fresh install or upgrade, the embedding model downloads automatically (~640 MB for most devices, larger on devices with more RAM). During the download, a full-screen overlay blocks all navigation with a "Downloading embedding model" message — this is a one-time download and typically takes a few minutes. Memory search is unavailable until the download completes.
|
|
537
|
+
|
|
536
538
|
When your assistant writes to **public/** or **shared/**, a shield icon appears in the navigation bar so you can review what was written (see [Data Safety Alert](#data-safety-alert) above).
|
|
537
539
|
|
|
538
540
|
### Searching Memory
|
|
@@ -1265,6 +1267,57 @@ If the page loses connection during the update and doesn't reconnect within two
|
|
|
1265
1267
|
|
|
1266
1268
|
---
|
|
1267
1269
|
|
|
1270
|
+
## Command Line (CLI)
|
|
1271
|
+
|
|
1272
|
+
Taskmaster includes a command-line tool called `taskmaster` that you can run in a terminal (Terminal on Mac, or via SSH on a Pi). You don't need the command line for day-to-day use — the Control Panel handles everything — but it's useful for troubleshooting, updating when the Control Panel isn't accessible, or automating tasks.
|
|
1273
|
+
|
|
1274
|
+
### How to access the terminal
|
|
1275
|
+
|
|
1276
|
+
- **Mac:** Open **Terminal** (search for "Terminal" in Spotlight)
|
|
1277
|
+
- **Raspberry Pi (direct):** Plug in a keyboard and monitor, open Terminal from the taskbar
|
|
1278
|
+
- **Raspberry Pi (remote):** From another computer, run `ssh admin@taskmaster.local` (replace `taskmaster` with your hostname if changed)
|
|
1279
|
+
|
|
1280
|
+
### Essential commands
|
|
1281
|
+
|
|
1282
|
+
| Command | What it does |
|
|
1283
|
+
|---------|-------------|
|
|
1284
|
+
| `taskmaster update` | Update to the latest version and restart the service |
|
|
1285
|
+
| `taskmaster daemon restart` | Restart the gateway service (picks up code and config changes) |
|
|
1286
|
+
| `taskmaster daemon status` | Check if the gateway service is running |
|
|
1287
|
+
| `taskmaster doctor` | Run health checks and fix common issues automatically |
|
|
1288
|
+
| `taskmaster status` | Show connection status for all channels (WhatsApp, Claude, etc.) |
|
|
1289
|
+
| `taskmaster dashboard` | Open the Control Panel in your browser |
|
|
1290
|
+
|
|
1291
|
+
### Configuration commands
|
|
1292
|
+
|
|
1293
|
+
| Command | What it does |
|
|
1294
|
+
|---------|-------------|
|
|
1295
|
+
| `taskmaster config set <key> <value>` | Change a config setting (e.g., `taskmaster config set gateway.port 19000`) |
|
|
1296
|
+
| `taskmaster config get <key>` | Read a config setting |
|
|
1297
|
+
| `taskmaster config` | Open the interactive config wizard |
|
|
1298
|
+
|
|
1299
|
+
### When to use the CLI instead of the Control Panel
|
|
1300
|
+
|
|
1301
|
+
- **Can't reach the Control Panel** — If the web UI is down, SSH into the device and use CLI commands to restart or update
|
|
1302
|
+
- **Updating from a specific version** — `taskmaster update` works even when the Control Panel can't load
|
|
1303
|
+
- **Changing network settings** — Port and hostname changes require the terminal (see "Changing Network Settings" above)
|
|
1304
|
+
- **Diagnosing problems** — `taskmaster doctor` runs automated health checks that can detect and fix issues the UI can't show
|
|
1305
|
+
- **Scripting and automation** — CLI commands can be combined in scripts for advanced setups
|
|
1306
|
+
|
|
1307
|
+
### Getting help for any command
|
|
1308
|
+
|
|
1309
|
+
Add `--help` to any command to see its options:
|
|
1310
|
+
|
|
1311
|
+
```bash
|
|
1312
|
+
taskmaster update --help
|
|
1313
|
+
taskmaster daemon --help
|
|
1314
|
+
taskmaster config set --help
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
Or run `taskmaster help` to see all available commands.
|
|
1318
|
+
|
|
1319
|
+
---
|
|
1320
|
+
|
|
1268
1321
|
## Uninstalling Taskmaster
|
|
1269
1322
|
|
|
1270
1323
|
### From the Control Panel
|