@rubytech/taskmaster 1.0.66 → 1.0.68
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-Uo_tQYx1.css → index-BCh3mx9Z.css} +1 -1
- package/dist/control-ui/assets/{index-8pJBjxcK.js → index-Tpr1NFEw.js} +162 -162
- package/dist/control-ui/assets/index-Tpr1NFEw.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/net.js +25 -18
- package/dist/gateway/public-chat/session.js +16 -24
- package/dist/gateway/server-methods/memory.js +2 -0
- package/dist/memory/embeddings-gemini.js +55 -23
- package/dist/memory/embeddings.js +35 -23
- package/dist/memory/manager.js +14 -1
- package/package.json +1 -1
- package/taskmaster-docs/USER-GUIDE.md +51 -0
- package/dist/control-ui/assets/index-8pJBjxcK.js.map +0 -1
|
@@ -6,8 +6,8 @@
|
|
|
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-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-Tpr1NFEw.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BCh3mx9Z.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<taskmaster-app></taskmaster-app>
|
package/dist/gateway/net.js
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js";
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
8
9
|
*/
|
|
9
|
-
function
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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;
|
|
20
27
|
}
|
|
21
28
|
export function isLoopbackAddress(ip) {
|
|
22
29
|
if (!ip)
|
|
@@ -96,9 +103,9 @@ export function isLocalGatewayAddress(ip) {
|
|
|
96
103
|
const tailnetIPv6 = pickPrimaryTailnetIPv6();
|
|
97
104
|
if (tailnetIPv6 && ip.trim().toLowerCase() === tailnetIPv6.toLowerCase())
|
|
98
105
|
return true;
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
if (
|
|
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))
|
|
102
109
|
return true;
|
|
103
110
|
return false;
|
|
104
111
|
}
|
|
@@ -5,35 +5,27 @@
|
|
|
5
5
|
* Anonymous sessions use a cookie-based identifier; verified sessions use the
|
|
6
6
|
* phone number so they share the same DM session as WhatsApp.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { listBoundAccountIds } from "../../routing/bindings.js";
|
|
9
|
+
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
|
9
10
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
|
10
11
|
/**
|
|
11
12
|
* Find the agent that handles public-facing WhatsApp DMs.
|
|
12
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* Uses the same routing logic as WhatsApp itself: calls resolveAgentRoute
|
|
15
|
+
* with a synthetic unknown-peer DM on the first WhatsApp account. This
|
|
16
|
+
* guarantees the public chat routes to the exact same agent that handles
|
|
17
|
+
* unknown WhatsApp DMs.
|
|
13
18
|
*/
|
|
14
19
|
export function resolvePublicAgentId(cfg) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Any whatsapp binding
|
|
25
|
-
for (const binding of bindings) {
|
|
26
|
-
if (binding.match.channel === "whatsapp") {
|
|
27
|
-
return normalizeAgentId(binding.agentId);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Agent explicitly named "public"
|
|
31
|
-
const agents = cfg.agents?.list ?? [];
|
|
32
|
-
const publicAgent = agents.find((a) => a.id === "public");
|
|
33
|
-
if (publicAgent)
|
|
34
|
-
return normalizeAgentId(publicAgent.id);
|
|
35
|
-
// Fall back to default agent
|
|
36
|
-
return resolveDefaultAgentId(cfg);
|
|
20
|
+
const accountIds = listBoundAccountIds(cfg, "whatsapp");
|
|
21
|
+
const accountId = accountIds[0] ?? "default";
|
|
22
|
+
const route = resolveAgentRoute({
|
|
23
|
+
cfg,
|
|
24
|
+
channel: "whatsapp",
|
|
25
|
+
accountId,
|
|
26
|
+
peer: { kind: "dm", id: "__public_chat__" },
|
|
27
|
+
});
|
|
28
|
+
return normalizeAgentId(route.agentId);
|
|
37
29
|
}
|
|
38
30
|
/**
|
|
39
31
|
* Build the session key for a public-chat visitor.
|
|
@@ -41,6 +41,14 @@ function normalizeGeminiBaseUrl(raw) {
|
|
|
41
41
|
function buildGeminiModelPath(model) {
|
|
42
42
|
return model.startsWith("models/") ? model : `models/${model}`;
|
|
43
43
|
}
|
|
44
|
+
/** Extract retry delay from a Gemini 429 response body, defaulting to 60s. */
|
|
45
|
+
function parseRetryDelay(body) {
|
|
46
|
+
// Gemini includes "retryDelay": "52s" in the response.
|
|
47
|
+
const match = body.match(/"retryDelay"\s*:\s*"(\d+)s?"/);
|
|
48
|
+
if (match)
|
|
49
|
+
return Math.max(1, Number(match[1]));
|
|
50
|
+
return 60;
|
|
51
|
+
}
|
|
44
52
|
export async function createGeminiEmbeddingProvider(options) {
|
|
45
53
|
const client = await resolveGeminiEmbeddingClient(options);
|
|
46
54
|
const baseUrl = client.baseUrl.replace(/\/$/, "");
|
|
@@ -49,20 +57,31 @@ export async function createGeminiEmbeddingProvider(options) {
|
|
|
49
57
|
const embedQuery = async (text) => {
|
|
50
58
|
if (!text.trim())
|
|
51
59
|
return [];
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
body: JSON.stringify({
|
|
56
|
-
content: { parts: [{ text }] },
|
|
57
|
-
taskType: "RETRIEVAL_QUERY",
|
|
58
|
-
}),
|
|
60
|
+
const body = JSON.stringify({
|
|
61
|
+
content: { parts: [{ text }] },
|
|
62
|
+
taskType: "RETRIEVAL_QUERY",
|
|
59
63
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
const maxRetries = 3;
|
|
65
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
66
|
+
const res = await fetch(embedUrl, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: client.headers,
|
|
69
|
+
body,
|
|
70
|
+
});
|
|
71
|
+
if (res.status === 429 && attempt < maxRetries) {
|
|
72
|
+
const retryAfter = parseRetryDelay(await res.text());
|
|
73
|
+
log.info(`gemini rate limit hit; retrying in ${retryAfter}s`);
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const payload = await res.text();
|
|
79
|
+
throw new Error(`gemini embeddings failed: ${res.status} ${payload}`);
|
|
80
|
+
}
|
|
81
|
+
const payload = (await res.json());
|
|
82
|
+
return payload.embedding?.values ?? [];
|
|
63
83
|
}
|
|
64
|
-
|
|
65
|
-
return payload.embedding?.values ?? [];
|
|
84
|
+
throw new Error("gemini embeddings: exhausted retries after rate limiting");
|
|
66
85
|
};
|
|
67
86
|
const embedBatch = async (texts) => {
|
|
68
87
|
if (texts.length === 0)
|
|
@@ -72,18 +91,31 @@ export async function createGeminiEmbeddingProvider(options) {
|
|
|
72
91
|
content: { parts: [{ text }] },
|
|
73
92
|
taskType: "RETRIEVAL_DOCUMENT",
|
|
74
93
|
}));
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
const body = JSON.stringify({ requests });
|
|
95
|
+
// Retry on 429 (rate limit) — Gemini free tier caps at 100 requests/minute.
|
|
96
|
+
// The initial bulk index can exceed this; retrying after the cooldown lets it complete.
|
|
97
|
+
const maxRetries = 3;
|
|
98
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
99
|
+
const res = await fetch(batchUrl, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: client.headers,
|
|
102
|
+
body,
|
|
103
|
+
});
|
|
104
|
+
if (res.status === 429 && attempt < maxRetries) {
|
|
105
|
+
const retryAfter = parseRetryDelay(await res.text());
|
|
106
|
+
log.info(`gemini rate limit hit; retrying in ${retryAfter}s`);
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const payload = await res.text();
|
|
112
|
+
throw new Error(`gemini embeddings failed: ${res.status} ${payload}`);
|
|
113
|
+
}
|
|
114
|
+
const payload = (await res.json());
|
|
115
|
+
const embeddings = Array.isArray(payload.embeddings) ? payload.embeddings : [];
|
|
116
|
+
return texts.map((_, index) => embeddings[index]?.values ?? []);
|
|
83
117
|
}
|
|
84
|
-
|
|
85
|
-
const embeddings = Array.isArray(payload.embeddings) ? payload.embeddings : [];
|
|
86
|
-
return texts.map((_, index) => embeddings[index]?.values ?? []);
|
|
118
|
+
throw new Error("gemini embeddings: exhausted retries after rate limiting");
|
|
87
119
|
};
|
|
88
120
|
return {
|
|
89
121
|
provider: {
|
|
@@ -6,16 +6,15 @@ import { createGeminiEmbeddingProvider } from "./embeddings-gemini.js";
|
|
|
6
6
|
import { createOpenAiEmbeddingProvider } from "./embeddings-openai.js";
|
|
7
7
|
import { importNodeLlamaCpp } from "./node-llama.js";
|
|
8
8
|
/**
|
|
9
|
-
* Default local embedding model.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* enormously across GPU vendors and Metal/Vulkan backends.
|
|
9
|
+
* Default local embedding model. embeddinggemma (329 MB) is proven stable on
|
|
10
|
+
* Intel x64 Macs and ARM Pis via node-llama-cpp's CPU backend. Larger models
|
|
11
|
+
* (Qwen3-Embedding 0.6B/4B/8B) can be configured explicitly via
|
|
12
|
+
* `local.modelPath` but are untested across hardware — Qwen3 models caused
|
|
13
|
+
* runaway memory consumption (40-76 GB) on Intel x64 Mac + AMD GPU.
|
|
15
14
|
*/
|
|
16
15
|
const DEFAULT_LOCAL_MODEL = {
|
|
17
|
-
model: "hf:
|
|
18
|
-
label: "
|
|
16
|
+
model: "hf:ggml-org/embeddinggemma-300M-Q8_0-GGUF/embeddinggemma-300M-Q8_0.gguf",
|
|
17
|
+
label: "embeddinggemma-300M",
|
|
19
18
|
};
|
|
20
19
|
function selectDefaultLocalModel() {
|
|
21
20
|
return DEFAULT_LOCAL_MODEL;
|
|
@@ -174,36 +173,49 @@ export async function createEmbeddingProvider(options) {
|
|
|
174
173
|
};
|
|
175
174
|
const formatPrimaryError = (err, provider) => provider === "local" ? formatLocalSetupError(err) : formatError(err);
|
|
176
175
|
if (requestedProvider === "auto") {
|
|
177
|
-
const
|
|
178
|
-
|
|
176
|
+
const errors = [];
|
|
177
|
+
// 1. If user configured an explicit local model file, try it first.
|
|
179
178
|
if (canAutoSelectLocal(options)) {
|
|
180
179
|
try {
|
|
181
180
|
const local = await createProvider("local");
|
|
182
181
|
return { ...local, requestedProvider };
|
|
183
182
|
}
|
|
184
183
|
catch (err) {
|
|
185
|
-
|
|
184
|
+
errors.push(formatLocalSetupError(err));
|
|
186
185
|
}
|
|
187
186
|
}
|
|
188
|
-
|
|
187
|
+
// 2. Try remote providers — preferred when API keys are available
|
|
188
|
+
// (faster, no RAM overhead, no model download).
|
|
189
|
+
// Only consider providers whose key is in config.apiKeys.
|
|
190
|
+
// Environment variables are ignored for auto-selection to avoid
|
|
191
|
+
// stale dev keys that would fail at runtime.
|
|
192
|
+
const configKeys = options.config.apiKeys ?? {};
|
|
193
|
+
const remoteProviders = [
|
|
194
|
+
{ id: "openai", configKey: "openai" },
|
|
195
|
+
{ id: "gemini", configKey: "google" },
|
|
196
|
+
];
|
|
197
|
+
for (const { id, configKey } of remoteProviders) {
|
|
198
|
+
if (!configKeys[configKey]?.trim())
|
|
199
|
+
continue;
|
|
189
200
|
try {
|
|
190
|
-
const result = await createProvider(
|
|
201
|
+
const result = await createProvider(id);
|
|
191
202
|
return { ...result, requestedProvider };
|
|
192
203
|
}
|
|
193
204
|
catch (err) {
|
|
194
|
-
|
|
195
|
-
if (isMissingApiKeyError(err)) {
|
|
196
|
-
missingKeyErrors.push(message);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
throw new Error(message);
|
|
205
|
+
errors.push(formatPrimaryError(err, id));
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
// 3. Fall back to local (downloads default model on first use).
|
|
209
|
+
try {
|
|
210
|
+
const local = await createProvider("local");
|
|
211
|
+
return { ...local, requestedProvider };
|
|
205
212
|
}
|
|
206
|
-
|
|
213
|
+
catch (err) {
|
|
214
|
+
errors.push(formatLocalSetupError(err));
|
|
215
|
+
}
|
|
216
|
+
throw new Error(errors.length > 0
|
|
217
|
+
? errors.join("\n\n")
|
|
218
|
+
: "No embeddings provider available.");
|
|
207
219
|
}
|
|
208
220
|
try {
|
|
209
221
|
const primary = await createProvider(requestedProvider);
|
package/dist/memory/manager.js
CHANGED
|
@@ -282,6 +282,7 @@ export class MemoryIndexManager {
|
|
|
282
282
|
sessionDeltas = new Map();
|
|
283
283
|
sessionWarm = new Set();
|
|
284
284
|
syncing = null;
|
|
285
|
+
syncProgress = null;
|
|
285
286
|
/**
|
|
286
287
|
* Ensure standard memory directory structure exists.
|
|
287
288
|
* Creates: memory/public, memory/shared, memory/admin, memory/users
|
|
@@ -509,8 +510,18 @@ export class MemoryIndexManager {
|
|
|
509
510
|
async sync(params) {
|
|
510
511
|
if (this.syncing)
|
|
511
512
|
return this.syncing;
|
|
512
|
-
this.
|
|
513
|
+
this.syncProgress = { completed: 0, total: 0 };
|
|
514
|
+
const outerProgress = params?.progress;
|
|
515
|
+
const wrappedParams = {
|
|
516
|
+
...params,
|
|
517
|
+
progress: (update) => {
|
|
518
|
+
this.syncProgress = { completed: update.completed, total: update.total };
|
|
519
|
+
outerProgress?.(update);
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
this.syncing = this.runSync(wrappedParams).finally(() => {
|
|
513
523
|
this.syncing = null;
|
|
524
|
+
this.syncProgress = null;
|
|
514
525
|
});
|
|
515
526
|
return this.syncing;
|
|
516
527
|
}
|
|
@@ -663,6 +674,8 @@ export class MemoryIndexManager {
|
|
|
663
674
|
files: files?.c ?? 0,
|
|
664
675
|
chunks: chunks?.c ?? 0,
|
|
665
676
|
dirty: this.dirty,
|
|
677
|
+
syncing: this.syncing !== null,
|
|
678
|
+
syncProgress: this.syncProgress ?? undefined,
|
|
666
679
|
workspaceDir: this.workspaceDir,
|
|
667
680
|
dbPath: this.settings.store.path,
|
|
668
681
|
provider: this.provider.id,
|
package/package.json
CHANGED
|
@@ -1267,6 +1267,57 @@ If the page loses connection during the update and doesn't reconnect within two
|
|
|
1267
1267
|
|
|
1268
1268
|
---
|
|
1269
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
|
+
|
|
1270
1321
|
## Uninstalling Taskmaster
|
|
1271
1322
|
|
|
1272
1323
|
### From the Control Panel
|