@rubytech/taskmaster 1.19.1 → 1.20.0
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/skills/frontmatter.js +79 -0
- package/dist/agents/skills/workspace.js +4 -0
- package/dist/agents/skills-status.js +18 -3
- package/dist/agents/system-prompt.js +6 -4
- package/dist/agents/taskmaster-tools.js +4 -0
- package/dist/agents/tool-policy.js +2 -1
- package/dist/agents/tools/skill-pack-install-tool.js +90 -0
- package/dist/agents/tools/skill-pack-sign-tool.js +91 -0
- package/dist/build-info.json +3 -3
- package/dist/control-ui/assets/{index-mjAT1dyG.js → index-DbeiXb7c.js} +555 -445
- package/dist/control-ui/assets/index-DbeiXb7c.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/gateway/protocol/schema/agents-models-skills.js +3 -0
- package/dist/gateway/server-methods/auth.js +40 -0
- package/dist/gateway/server-methods/skills.js +7 -2
- package/dist/hooks/bundled/ride-dispatch/handler.js +1 -1
- package/dist/license/skill-pack.js +199 -0
- package/package.json +1 -1
- package/skills/skill-pack-distribution/SKILL.md +27 -0
- package/skills/taskmaster/SKILL.md +2 -2
- package/taskmaster-docs/USER-GUIDE.md +78 -9
- package/templates/beagle-zanzibar/agents/public/AGENTS.md +2 -2
- package/dist/control-ui/assets/index-mjAT1dyG.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-DbeiXb7c.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-0WHVrpg7.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -60,6 +60,9 @@ export const SkillsCreateParamsSchema = Type.Object({
|
|
|
60
60
|
}),
|
|
61
61
|
skillContent: NonEmptyString,
|
|
62
62
|
references: Type.Optional(Type.Array(SkillReferenceFileSchema)),
|
|
63
|
+
alwaysActive: Type.Optional(Type.Boolean({
|
|
64
|
+
description: "When true, the skill is embedded in every conversation instead of loaded on demand",
|
|
65
|
+
})),
|
|
63
66
|
}, { additionalProperties: false });
|
|
64
67
|
export const SkillsDeleteParamsSchema = Type.Object({
|
|
65
68
|
name: NonEmptyString,
|
|
@@ -47,6 +47,7 @@ export const authHandlers = {
|
|
|
47
47
|
provider: "anthropic",
|
|
48
48
|
email: profile.email,
|
|
49
49
|
message: "Connected",
|
|
50
|
+
...(profile.type === "oauth" && profile.refresh ? { refreshToken: profile.refresh } : {}),
|
|
50
51
|
});
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
@@ -85,6 +86,7 @@ export const authHandlers = {
|
|
|
85
86
|
message: isExpired
|
|
86
87
|
? "Connection expired — click to reconnect"
|
|
87
88
|
: `Connected · expires in ${expiresInMinutes}m`,
|
|
89
|
+
...(profile.type === "oauth" && profile.refresh ? { refreshToken: profile.refresh } : {}),
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
92
|
catch (err) {
|
|
@@ -280,4 +282,42 @@ export const authHandlers = {
|
|
|
280
282
|
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)));
|
|
281
283
|
}
|
|
282
284
|
},
|
|
285
|
+
/**
|
|
286
|
+
* Import a refresh token from another device
|
|
287
|
+
* Validates the token by minting fresh credentials, then stores them
|
|
288
|
+
*/
|
|
289
|
+
"auth.importRefreshToken": async ({ params, respond }) => {
|
|
290
|
+
try {
|
|
291
|
+
const { refreshToken } = params;
|
|
292
|
+
if (!refreshToken || typeof refreshToken !== "string") {
|
|
293
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "Missing refreshToken"));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// Validate the token by using it to mint fresh credentials
|
|
297
|
+
const credentials = await refreshAnthropicToken(refreshToken);
|
|
298
|
+
upsertAuthProfile({
|
|
299
|
+
profileId: CLAUDE_CLI_PROFILE_ID,
|
|
300
|
+
credential: {
|
|
301
|
+
type: "oauth",
|
|
302
|
+
provider: "anthropic",
|
|
303
|
+
access: credentials.access,
|
|
304
|
+
refresh: credentials.refresh,
|
|
305
|
+
expires: credentials.expires,
|
|
306
|
+
email: credentials.email,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
writeClaudeCliCredentials(credentials);
|
|
310
|
+
const expiresInMinutes = Math.max(0, Math.round((credentials.expires - Date.now()) / 60000));
|
|
311
|
+
respond(true, {
|
|
312
|
+
connected: true,
|
|
313
|
+
provider: "anthropic",
|
|
314
|
+
email: credentials.email,
|
|
315
|
+
expiresIn: expiresInMinutes,
|
|
316
|
+
message: "Connected via imported refresh token",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, `Invalid refresh token: ${formatForLog(err)}`));
|
|
321
|
+
}
|
|
322
|
+
},
|
|
283
323
|
};
|
|
@@ -4,6 +4,7 @@ import { resolveAgentWorkspaceDir, resolveAgentWorkspaceRoot, resolveDefaultAgen
|
|
|
4
4
|
import { installSkill } from "../../agents/skills-install.js";
|
|
5
5
|
import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
|
|
6
6
|
import { loadWorkspaceSkillEntries, resolveBundledSkillsDir, } from "../../agents/skills.js";
|
|
7
|
+
import { extractEmbedFlag, applyEmbedFlag } from "../../agents/skills/frontmatter.js";
|
|
7
8
|
import { bumpSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
|
8
9
|
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
|
9
10
|
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
|
@@ -170,6 +171,7 @@ export const skillsHandlers = {
|
|
|
170
171
|
return;
|
|
171
172
|
}
|
|
172
173
|
const content = fs.readFileSync(skillFile, "utf-8");
|
|
174
|
+
const alwaysActive = extractEmbedFlag(content);
|
|
173
175
|
const references = [];
|
|
174
176
|
const refsDir = path.join(skillDir, "references");
|
|
175
177
|
if (fs.existsSync(refsDir) && fs.statSync(refsDir).isDirectory()) {
|
|
@@ -183,7 +185,7 @@ export const skillsHandlers = {
|
|
|
183
185
|
}
|
|
184
186
|
references.sort((a, b) => a.name.localeCompare(b.name));
|
|
185
187
|
}
|
|
186
|
-
respond(true, { name: p.name, content, references }, undefined);
|
|
188
|
+
respond(true, { name: p.name, content, references, alwaysActive }, undefined);
|
|
187
189
|
},
|
|
188
190
|
"skills.create": async ({ params, respond }) => {
|
|
189
191
|
if (!validateSkillsCreateParams(params)) {
|
|
@@ -203,7 +205,10 @@ export const skillsHandlers = {
|
|
|
203
205
|
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
204
206
|
}
|
|
205
207
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
206
|
-
|
|
208
|
+
const finalContent = typeof p.alwaysActive === "boolean"
|
|
209
|
+
? applyEmbedFlag(p.skillContent, p.alwaysActive)
|
|
210
|
+
: p.skillContent;
|
|
211
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), finalContent, "utf-8");
|
|
207
212
|
if (p.references && p.references.length > 0) {
|
|
208
213
|
const refsDir = path.join(skillDir, "references");
|
|
209
214
|
fs.mkdirSync(refsDir, { recursive: true });
|
|
@@ -375,7 +375,7 @@ function buildPaymentConfirmedInstruction(params) {
|
|
|
375
375
|
` body: "[${bookingId}] You have a confirmed booking.\n` +
|
|
376
376
|
`Pickup: [pickup] → [destination]\n` +
|
|
377
377
|
`Time: [time] on [date]\n` +
|
|
378
|
-
`Passenger: [tourist_name]\n` +
|
|
378
|
+
`Passenger: [tourist_name] — WhatsApp: +[tourist_phone]\n` +
|
|
379
379
|
`Fare: $[fare]\n` +
|
|
380
380
|
`\n` +
|
|
381
381
|
`Your pickup PIN is [pin]. When you meet your passenger, say: 'Your PIN is [pin].' They will confirm it matches theirs. The QR code above is a backup they can scan instead."\n\n` +
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Canonical JSON
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/** Build the canonical JSON string that gets signed. Deterministic key order. */
|
|
9
|
+
export function buildCanonicalPayload(payload) {
|
|
10
|
+
const ordered = {
|
|
11
|
+
format: payload.format,
|
|
12
|
+
pack: {
|
|
13
|
+
id: payload.pack.id,
|
|
14
|
+
version: payload.pack.version,
|
|
15
|
+
name: payload.pack.name,
|
|
16
|
+
description: payload.pack.description,
|
|
17
|
+
author: payload.pack.author,
|
|
18
|
+
},
|
|
19
|
+
device: {
|
|
20
|
+
did: payload.device.did,
|
|
21
|
+
cid: payload.device.cid,
|
|
22
|
+
},
|
|
23
|
+
content: {
|
|
24
|
+
skill: payload.content.skill,
|
|
25
|
+
references: payload.content.references.map((r) => ({
|
|
26
|
+
name: r.name,
|
|
27
|
+
content: r.content,
|
|
28
|
+
})),
|
|
29
|
+
},
|
|
30
|
+
signedAt: payload.signedAt,
|
|
31
|
+
};
|
|
32
|
+
return JSON.stringify(ordered);
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Signing
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
function loadSigningKey() {
|
|
38
|
+
const raw = process.env.LICENSE_SIGNING_KEY;
|
|
39
|
+
if (!raw) {
|
|
40
|
+
throw new Error("LICENSE_SIGNING_KEY not set. Cannot sign skill packs.");
|
|
41
|
+
}
|
|
42
|
+
const pem = raw.replace(/\\n/g, "\n");
|
|
43
|
+
return crypto.createPrivateKey(pem);
|
|
44
|
+
}
|
|
45
|
+
/** Sign a skill pack payload with the Ed25519 private key from LICENSE_SIGNING_KEY. */
|
|
46
|
+
export function signSkillPack(payload) {
|
|
47
|
+
const privateKey = loadSigningKey();
|
|
48
|
+
const canonical = buildCanonicalPayload(payload);
|
|
49
|
+
const signature = crypto.sign(null, Buffer.from(canonical), privateKey);
|
|
50
|
+
return {
|
|
51
|
+
...payload,
|
|
52
|
+
signature: signature.toString("base64url"),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Verification
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
59
|
+
MCowBQYDK2VwAyEA/t/C4A4I0rDlj5rEqv6Hy6VdHJr7WiJHWUxgwGz9HcM=
|
|
60
|
+
-----END PUBLIC KEY-----`;
|
|
61
|
+
const publicKey = crypto.createPublicKey(PUBLIC_KEY_PEM);
|
|
62
|
+
/** Verify a skill pack file's structure and Ed25519 signature. */
|
|
63
|
+
export function verifySkillPack(raw) {
|
|
64
|
+
if (raw === null || typeof raw !== "object") {
|
|
65
|
+
return { valid: false, message: "Skill pack must be a JSON object" };
|
|
66
|
+
}
|
|
67
|
+
const obj = raw;
|
|
68
|
+
// Check signature field exists
|
|
69
|
+
if (typeof obj.signature !== "string" || !obj.signature) {
|
|
70
|
+
return { valid: false, message: "Missing or invalid signature" };
|
|
71
|
+
}
|
|
72
|
+
// Validate format
|
|
73
|
+
if (obj.format !== "skillpack-v1") {
|
|
74
|
+
return { valid: false, message: "Unsupported skill pack format" };
|
|
75
|
+
}
|
|
76
|
+
// Validate pack metadata
|
|
77
|
+
const pack = obj.pack;
|
|
78
|
+
if (!pack || typeof pack !== "object") {
|
|
79
|
+
return { valid: false, message: "Missing pack metadata" };
|
|
80
|
+
}
|
|
81
|
+
for (const key of ["id", "version", "name", "author"]) {
|
|
82
|
+
if (typeof pack[key] !== "string" || !pack[key]) {
|
|
83
|
+
return { valid: false, message: `Missing or invalid pack.${key}` };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (typeof pack.description !== "string") {
|
|
87
|
+
return { valid: false, message: "Missing or invalid pack.description" };
|
|
88
|
+
}
|
|
89
|
+
// Validate device
|
|
90
|
+
const device = obj.device;
|
|
91
|
+
if (!device || typeof device !== "object") {
|
|
92
|
+
return { valid: false, message: "Missing device info" };
|
|
93
|
+
}
|
|
94
|
+
if (typeof device.did !== "string" || !device.did) {
|
|
95
|
+
return { valid: false, message: "Missing or invalid device.did" };
|
|
96
|
+
}
|
|
97
|
+
if (typeof device.cid !== "string" || !device.cid) {
|
|
98
|
+
return { valid: false, message: "Missing or invalid device.cid" };
|
|
99
|
+
}
|
|
100
|
+
// Validate content
|
|
101
|
+
const content = obj.content;
|
|
102
|
+
if (!content || typeof content !== "object") {
|
|
103
|
+
return { valid: false, message: "Missing content" };
|
|
104
|
+
}
|
|
105
|
+
if (typeof content.skill !== "string") {
|
|
106
|
+
return { valid: false, message: "Missing or invalid content.skill" };
|
|
107
|
+
}
|
|
108
|
+
if (!Array.isArray(content.references)) {
|
|
109
|
+
return { valid: false, message: "Missing or invalid content.references" };
|
|
110
|
+
}
|
|
111
|
+
for (let i = 0; i < content.references.length; i++) {
|
|
112
|
+
const ref = content.references[i];
|
|
113
|
+
if (!ref || typeof ref !== "object") {
|
|
114
|
+
return { valid: false, message: `Invalid reference at index ${i}` };
|
|
115
|
+
}
|
|
116
|
+
if (typeof ref.name !== "string" || !ref.name) {
|
|
117
|
+
return { valid: false, message: `Missing or invalid reference name at index ${i}` };
|
|
118
|
+
}
|
|
119
|
+
if (typeof ref.content !== "string") {
|
|
120
|
+
return { valid: false, message: `Missing or invalid reference content at index ${i}` };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Validate signedAt
|
|
124
|
+
if (typeof obj.signedAt !== "string" || !obj.signedAt) {
|
|
125
|
+
return { valid: false, message: "Missing or invalid signedAt" };
|
|
126
|
+
}
|
|
127
|
+
// Rebuild payload (without signature) for verification
|
|
128
|
+
const payload = {
|
|
129
|
+
format: obj.format,
|
|
130
|
+
pack: {
|
|
131
|
+
id: pack.id,
|
|
132
|
+
version: pack.version,
|
|
133
|
+
name: pack.name,
|
|
134
|
+
description: pack.description,
|
|
135
|
+
author: pack.author,
|
|
136
|
+
},
|
|
137
|
+
device: {
|
|
138
|
+
did: device.did,
|
|
139
|
+
cid: device.cid,
|
|
140
|
+
},
|
|
141
|
+
content: {
|
|
142
|
+
skill: content.skill,
|
|
143
|
+
references: content.references.map((r) => ({
|
|
144
|
+
name: r.name,
|
|
145
|
+
content: r.content,
|
|
146
|
+
})),
|
|
147
|
+
},
|
|
148
|
+
signedAt: obj.signedAt,
|
|
149
|
+
};
|
|
150
|
+
// Verify Ed25519 signature
|
|
151
|
+
const canonical = buildCanonicalPayload(payload);
|
|
152
|
+
let signatureValid;
|
|
153
|
+
try {
|
|
154
|
+
const signature = Buffer.from(obj.signature, "base64url");
|
|
155
|
+
signatureValid = crypto.verify(null, Buffer.from(canonical), publicKey, signature);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return { valid: false, message: "Signature verification failed" };
|
|
159
|
+
}
|
|
160
|
+
if (!signatureValid) {
|
|
161
|
+
return { valid: false, message: "Invalid skill pack signature" };
|
|
162
|
+
}
|
|
163
|
+
return { valid: true, payload };
|
|
164
|
+
}
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Marker file helpers
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
const MARKER_FILENAME = ".skillpack";
|
|
169
|
+
/** Return the absolute path to the marker file inside a skill directory. */
|
|
170
|
+
export function markerPath(skillDir) {
|
|
171
|
+
return path.join(skillDir, MARKER_FILENAME);
|
|
172
|
+
}
|
|
173
|
+
/** Synchronously check whether a skill directory has a .skillpack marker. */
|
|
174
|
+
export function hasMarker(skillDir) {
|
|
175
|
+
return fs.existsSync(markerPath(skillDir));
|
|
176
|
+
}
|
|
177
|
+
/** Read and parse the .skillpack marker, or return null if absent or invalid. */
|
|
178
|
+
export async function readMarker(skillDir) {
|
|
179
|
+
try {
|
|
180
|
+
const raw = await fsp.readFile(markerPath(skillDir), "utf8");
|
|
181
|
+
const data = JSON.parse(raw);
|
|
182
|
+
if (!data.packId ||
|
|
183
|
+
!data.version ||
|
|
184
|
+
!data.author ||
|
|
185
|
+
!data.installedAt ||
|
|
186
|
+
!data.deviceId ||
|
|
187
|
+
!data.customerId) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return data;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Write a .skillpack marker file to a skill directory. */
|
|
197
|
+
export async function writeMarker(skillDir, marker) {
|
|
198
|
+
await fsp.writeFile(markerPath(skillDir), JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
199
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-pack-distribution
|
|
3
|
+
description: Sign and distribute licensed skill packs to customer devices
|
|
4
|
+
metadata: {"taskmaster":{"always":true,"emoji":"📦","skillKey":"skill-pack-distribution"}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Skill Pack Distribution
|
|
8
|
+
|
|
9
|
+
You can sign and send licensed skill packs to customers.
|
|
10
|
+
|
|
11
|
+
## Workflow
|
|
12
|
+
|
|
13
|
+
1. User asks to send a skill to a customer
|
|
14
|
+
2. Look up the customer's contact record to get their `device_id` and customer ID
|
|
15
|
+
3. Call `skill_pack_sign` with the pack ID, device ID, and customer ID
|
|
16
|
+
4. Send the resulting `.skillpack.json` file to the customer's WhatsApp number
|
|
17
|
+
5. Include a message: "Here's your [Skill Name] skill pack. Forward this file to your admin agent and ask it to install the skill."
|
|
18
|
+
|
|
19
|
+
## Available Packs
|
|
20
|
+
|
|
21
|
+
Check the `skill-packs/` folder in this workspace for available templates. Each subfolder is a pack ID containing SKILL.md and optional references/.
|
|
22
|
+
|
|
23
|
+
## Notes
|
|
24
|
+
|
|
25
|
+
- Each pack is signed for a specific device — it cannot be installed on other devices
|
|
26
|
+
- To update a customer's skill, sign and send a newer version (bump the version parameter)
|
|
27
|
+
- The customer's admin agent uses `skill_pack_install` to verify and install the pack
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: taskmaster
|
|
3
|
-
description: "Product knowledge for Taskmaster — answers questions about features, setup, channels, tools, skills, troubleshooting, and what's new by fetching live documentation."
|
|
3
|
+
description: "Product knowledge for Taskmaster — answers questions about features, setup, configuration, channels, tools, skills, troubleshooting (including Pi device issues like hostname, Chromium, networking, audio, display), architecture, and what's new by fetching live documentation."
|
|
4
4
|
metadata: {"taskmaster":{"always":true,"emoji":"📚","skillKey":"taskmaster"}}
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ You can answer questions about Taskmaster itself — the platform your assistant
|
|
|
10
10
|
|
|
11
11
|
## When This Applies
|
|
12
12
|
|
|
13
|
-
Activate when the user asks about Taskmaster features, setup, configuration, channels, tools, skills, troubleshooting, updates, or anything about the platform itself — not their business.
|
|
13
|
+
Activate when the user asks about Taskmaster features, setup, configuration, channels, tools, skills, troubleshooting, updates, or anything about the platform itself — not their business. This includes Pi device issues (hostname changes, Chromium, networking, audio, display, boot problems) since Taskmaster runs on a Pi and the user guide covers these topics.
|
|
14
14
|
|
|
15
15
|
## How to Answer
|
|
16
16
|
|
|
@@ -101,19 +101,63 @@ Your license is tied to this device. If you need to move it to a different devic
|
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
104
|
+
## Premium Setup
|
|
105
|
+
|
|
106
|
+
If you ordered a **Premium Setup**, your device has already been configured for you — installation, license activation, Claude account, agent profiles, skills, knowledge base, and branding are all in place. You can skip straight to using your assistant. Here's what you need to know:
|
|
107
|
+
|
|
108
|
+
### Accessing Your Control Panel
|
|
109
|
+
|
|
110
|
+
Your device hostname may have been customised during setup. If so, we will have communicated the address directly (e.g. `http://yourbusiness.local:18789`). The default `http://taskmaster.local:18789` will **not** work if the hostname was changed.
|
|
111
|
+
|
|
112
|
+
### Change Your PINs Immediately
|
|
113
|
+
|
|
114
|
+
Both your **admin PIN** and **account PIN** have been set to **123456** during setup. You must change these the first time you log in:
|
|
115
|
+
|
|
116
|
+
1. Open the control panel and enter **123456** when prompted
|
|
117
|
+
2. Go to **Setup** and scroll to the Security section
|
|
118
|
+
3. Change your **admin PIN** to something only you know (4–6 digits)
|
|
119
|
+
4. Change your **account PIN** to something different from your admin PIN
|
|
120
|
+
|
|
121
|
+
> **Why two PINs?** The admin PIN controls access to the full dashboard. The account PIN is for individual account access when multiple accounts are configured. See "Security & PINs" below for full details.
|
|
122
|
+
|
|
123
|
+
### What's Already Done
|
|
124
|
+
|
|
125
|
+
Depending on your package, some or all of the following will already be configured:
|
|
126
|
+
|
|
127
|
+
- **Claude account** connected and ready
|
|
128
|
+
- **Agent profiles** tailored to your business
|
|
129
|
+
- **Skills** loaded with industry-specific capabilities
|
|
130
|
+
- **Knowledge base** pre-loaded with your business information
|
|
131
|
+
- **Branding** (business name, colours, logo) applied
|
|
132
|
+
- **WhatsApp** linked (if included in your package)
|
|
133
|
+
|
|
134
|
+
### What You May Still Need to Do
|
|
135
|
+
|
|
136
|
+
- **Link WhatsApp** — if not included in your package, follow the "Link WhatsApp" step in the Setting Up section below
|
|
137
|
+
- **Add admin phone numbers** — go to the Admins page to add any phone numbers that should have admin access
|
|
138
|
+
- **Review your knowledge base** — go to Files and check what's been loaded; add or correct anything
|
|
139
|
+
|
|
140
|
+
Once your PINs are changed, you're ready to start chatting with your assistant.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
104
144
|
## Setting Up
|
|
105
145
|
|
|
106
|
-
Once your license is activated
|
|
146
|
+
Once your license is activated, you'll land on the **setup dashboard** where you can configure each part of your assistant at your own pace.
|
|
147
|
+
|
|
148
|
+
### 1. Connect to Claude (recommended)
|
|
149
|
+
|
|
150
|
+
Your assistant needs an AI provider to work. The easiest option is connecting your Claude Pro account:
|
|
107
151
|
|
|
108
|
-
|
|
152
|
+
1. On the setup dashboard, find the **Claude** row and tap the action button
|
|
153
|
+
2. In the modal that opens, tap **"Connect to Claude"**
|
|
154
|
+
3. Sign in with your Claude Pro account in the browser window that opens
|
|
155
|
+
4. Copy the code shown and paste it into the code field
|
|
156
|
+
5. Tap **Submit** — once connected, the Claude row turns green
|
|
109
157
|
|
|
110
|
-
|
|
111
|
-
2. Sign in with your Claude Pro account
|
|
112
|
-
3. Copy the code shown and paste it into the setup page
|
|
113
|
-
4. Tap **Submit**
|
|
114
|
-
5. When connected, tap **"Continue to Chat"** to start configuring your assistant
|
|
158
|
+
> **Managing multiple devices?** If you already have Claude connected on another Taskmaster device, you can copy the refresh token from that device's Claude info modal (tap the ⓘ icon next to Claude) and paste it into the **"Import refresh token"** section of the authentication modal on the new device. This avoids re-authenticating with Anthropic.
|
|
115
159
|
|
|
116
|
-
> **
|
|
160
|
+
> **Prefer an API key instead?** You can skip Claude OAuth entirely and enter an Anthropic API key (or keys for other providers like OpenAI or Google) in the **API Keys** section of the setup dashboard.
|
|
117
161
|
|
|
118
162
|
---
|
|
119
163
|
|
|
@@ -883,8 +927,16 @@ Each skill has a label:
|
|
|
883
927
|
| Label | Meaning |
|
|
884
928
|
|-------|---------|
|
|
885
929
|
| **Preloaded** | Ships with the product. Always active. Cannot be disabled or deleted. |
|
|
930
|
+
| **Licensed** | Installed from a skill pack sent by your provider. Can be deleted but not edited. |
|
|
886
931
|
| **User** | Created by you (or by your assistant on your behalf). You can enable, disable, edit, or delete these. |
|
|
887
932
|
|
|
933
|
+
Each user skill also shows a chip on its card: **Always active** or **On demand**. Tap the chip to switch between modes:
|
|
934
|
+
|
|
935
|
+
- **Always active** — the skill is part of every conversation. Your assistant always follows its instructions, whether the topic comes up or not. Use this for skills that shape how your assistant communicates (e.g. a sales approach or customer service style).
|
|
936
|
+
- **On demand** (default) — your assistant loads the skill only when a relevant topic comes up. Most skills should stay on demand to keep conversations focused.
|
|
937
|
+
|
|
938
|
+
Changes take effect on the next message — no restart needed.
|
|
939
|
+
|
|
888
940
|
#### Filtering the list
|
|
889
941
|
|
|
890
942
|
Use the **All / Preloaded / User** chip buttons at the top of the list to show only the type you're interested in.
|
|
@@ -897,7 +949,7 @@ Tap any skill to open its detail view. The detail view has tabs — one for the
|
|
|
897
949
|
|
|
898
950
|
From the detail view, tap **Edit** to open the skill editor. The editor has the same tabs as the detail view (SKILL.md and any reference files), so you can edit each part independently. If you are already viewing a reference tab when you tap Edit, the editor opens directly on that reference.
|
|
899
951
|
|
|
900
|
-
When you save an installed skill, the changes take effect immediately.
|
|
952
|
+
When you save an installed skill, the changes take effect immediately — no restart needed.
|
|
901
953
|
|
|
902
954
|
#### Enable / Disable
|
|
903
955
|
|
|
@@ -907,6 +959,23 @@ Toggle a user skill on or off with the switch on its card. A disabled skill stay
|
|
|
907
959
|
|
|
908
960
|
Tap the delete icon on a user skill's card. You will be asked to confirm before the skill is removed.
|
|
909
961
|
|
|
962
|
+
#### Skill Packs (Licensed Skills)
|
|
963
|
+
|
|
964
|
+
Your Taskmaster provider can send you additional skills as **skill packs** — signed files that install licensed capabilities onto your device. These are skills built specifically for your business or industry.
|
|
965
|
+
|
|
966
|
+
**How to install a skill pack:**
|
|
967
|
+
|
|
968
|
+
1. Your provider sends you a `.skillpack.json` file via WhatsApp
|
|
969
|
+
2. Forward that file to your **admin assistant** (the one you chat with directly)
|
|
970
|
+
3. Ask your assistant to install it — for example: "Please install this skill pack"
|
|
971
|
+
4. The assistant verifies the file is genuine and licensed for your device, then installs it
|
|
972
|
+
|
|
973
|
+
Once installed, the skill appears in the Skills tab with a **Licensed** badge.
|
|
974
|
+
|
|
975
|
+
**Updating a skill pack:** Your provider sends you a new version of the file. Follow the same steps — forwarding and asking your assistant to install. The new version replaces the old one automatically.
|
|
976
|
+
|
|
977
|
+
**Removing a licensed skill:** Delete it from the Skills tab the same way you would delete any user skill. Tap the delete icon on its card and confirm.
|
|
978
|
+
|
|
910
979
|
#### Drafts — creating new skills
|
|
911
980
|
|
|
912
981
|
You do not need to write skill files by hand. Instead:
|
|
@@ -61,7 +61,7 @@ All users are verified before they reach you — WhatsApp users by their phone n
|
|
|
61
61
|
|
|
62
62
|
### Step 1 — Capture the request
|
|
63
63
|
|
|
64
|
-
Gather: pickup location, destination, date/time, number of passengers, luggage, special requests. If the tourist gave everything in one message, proceed immediately. If anything is missing, ask —
|
|
64
|
+
Gather: first name, pickup location, destination, date/time, number of passengers, luggage, special requests. If the tourist gave everything in one message, proceed immediately. If anything is missing, ask — but do not ask for name as a separate question if the tourist has already provided it naturally in conversation. Then resume from Step 2 when they reply.
|
|
65
65
|
|
|
66
66
|
### Step 2 — Check knowledge base
|
|
67
67
|
|
|
@@ -80,7 +80,7 @@ Write a dispatch file via `memory_write` to `shared/dispatch/{job-id}-trip-reque
|
|
|
80
80
|
job_id: BGL-XXXX
|
|
81
81
|
phase: trip-request
|
|
82
82
|
tourist_phone: [the tourist's phone number — from WhatsApp session or OTP-verified phone]
|
|
83
|
-
tourist_name: [
|
|
83
|
+
tourist_name: [tourist's first name]
|
|
84
84
|
pickup: [pickup location]
|
|
85
85
|
destination: [destination]
|
|
86
86
|
date: [date]
|