@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.
@@ -120,3 +120,82 @@ export function resolveSkillInvocationPolicy(frontmatter) {
120
120
  export function resolveSkillKey(skill, entry) {
121
121
  return entry?.taskmaster?.skillKey ?? skill.name;
122
122
  }
123
+ /**
124
+ * Extract whether `embed: true` is set in the taskmaster metadata of a SKILL.md content string.
125
+ */
126
+ export function extractEmbedFlag(content) {
127
+ const frontmatter = parseFrontmatter(content);
128
+ const meta = resolveTaskmasterMetadata(frontmatter);
129
+ return meta?.embed === true;
130
+ }
131
+ /**
132
+ * Set or remove the `embed` flag in a SKILL.md content string's frontmatter metadata.
133
+ * Preserves other metadata fields. Handles the case where no metadata block exists yet.
134
+ */
135
+ export function applyEmbedFlag(content, embed) {
136
+ const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
137
+ // Parse existing metadata to preserve other fields.
138
+ const frontmatter = parseFrontmatter(normalized);
139
+ const rawMeta = frontmatter.metadata;
140
+ let metaObj = {};
141
+ if (rawMeta) {
142
+ try {
143
+ metaObj = JSON5.parse(rawMeta);
144
+ }
145
+ catch {
146
+ // If metadata is unparseable, start fresh.
147
+ }
148
+ }
149
+ // Update the taskmaster.embed field.
150
+ const taskmaster = metaObj.taskmaster && typeof metaObj.taskmaster === "object"
151
+ ? { ...metaObj.taskmaster }
152
+ : {};
153
+ if (embed) {
154
+ taskmaster.embed = true;
155
+ }
156
+ else {
157
+ delete taskmaster.embed;
158
+ }
159
+ // Clean up: if taskmaster object is empty, remove it; if metaObj is empty, remove metadata line.
160
+ const hasTaskmasterKeys = Object.keys(taskmaster).length > 0;
161
+ if (hasTaskmasterKeys) {
162
+ metaObj.taskmaster = taskmaster;
163
+ }
164
+ else {
165
+ delete metaObj.taskmaster;
166
+ }
167
+ const hasMetaKeys = Object.keys(metaObj).length > 0;
168
+ const newMetaValue = hasMetaKeys ? JSON.stringify(metaObj) : "";
169
+ // Now splice the metadata line into the frontmatter block.
170
+ if (!normalized.startsWith("---")) {
171
+ // No frontmatter at all — add one if we need metadata.
172
+ if (!newMetaValue)
173
+ return content;
174
+ return `---\nmetadata: ${newMetaValue}\n---\n${normalized}`;
175
+ }
176
+ const endIndex = normalized.indexOf("\n---", 3);
177
+ if (endIndex === -1)
178
+ return content; // Malformed frontmatter — don't touch.
179
+ const block = normalized.slice(4, endIndex);
180
+ const after = normalized.slice(endIndex + 4); // After closing ---
181
+ // Replace or add the metadata line within the frontmatter block.
182
+ const lines = block.split("\n");
183
+ let replaced = false;
184
+ const newLines = [];
185
+ for (const line of lines) {
186
+ if (/^metadata:\s/.test(line)) {
187
+ replaced = true;
188
+ if (newMetaValue) {
189
+ newLines.push(`metadata: ${newMetaValue}`);
190
+ }
191
+ // else: drop the line entirely (no metadata needed)
192
+ }
193
+ else {
194
+ newLines.push(line);
195
+ }
196
+ }
197
+ if (!replaced && newMetaValue) {
198
+ newLines.push(`metadata: ${newMetaValue}`);
199
+ }
200
+ return `---\n${newLines.join("\n")}\n---${after}`;
201
+ }
@@ -7,6 +7,7 @@ import { resolveBundledSkillsDir } from "./bundled-dir.js";
7
7
  import { shouldIncludeSkill } from "./config.js";
8
8
  import { parseFrontmatter, resolveTaskmasterMetadata, resolveSkillInvocationPolicy, } from "./frontmatter.js";
9
9
  import { resolvePluginSkillDirs } from "./plugin-skills.js";
10
+ import { hasMarker } from "../../license/skill-pack.js";
10
11
  import { serializeByKey } from "./serialize.js";
11
12
  const fsp = fs.promises;
12
13
  const skillsLogger = createSubsystemLogger("skills");
@@ -333,6 +334,9 @@ export async function syncBundledSkillsToWorkspace(workspaceDir, opts) {
333
334
  for (const name of entries) {
334
335
  const dest = path.join(targetSkillsDir, name);
335
336
  const exists = fs.existsSync(dest);
337
+ // Never overwrite marketplace-installed skills with bundled versions
338
+ if (exists && hasMarker(dest))
339
+ continue;
336
340
  if (exists && !FORCE_RESYNC_SKILLS.has(name))
337
341
  continue;
338
342
  try {
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { CONFIG_DIR } from "../utils.js";
4
+ import { hasMarker } from "../license/skill-pack.js";
4
5
  import { hasBinary, isBundledSkillAllowed, isConfigPathTruthy, loadWorkspaceSkillEntries, resolveBundledAllowlist, resolveBundledSkillsDir, resolveConfigPath, resolveSkillConfig, resolveSkillsInstallPreferences, } from "./skills.js";
5
6
  function resolveSkillKey(entry) {
6
7
  return entry.taskmaster?.skillKey ?? entry.skill.name;
@@ -84,6 +85,7 @@ function buildSkillStatus(entry, config, prefs, eligibility, bundledSkillNames)
84
85
  const allowBundled = resolveBundledAllowlist(config);
85
86
  const blockedByAllowlist = !isBundledSkillAllowed(entry, allowBundled);
86
87
  const always = entry.taskmaster?.always === true;
88
+ const alwaysActive = entry.taskmaster?.embed === true;
87
89
  const emoji = entry.taskmaster?.emoji ?? entry.frontmatter.emoji;
88
90
  const homepageRaw = entry.taskmaster?.homepage ??
89
91
  entry.frontmatter.homepage ??
@@ -146,9 +148,19 @@ function buildSkillStatus(entry, config, prefs, eligibility, bundledSkillNames)
146
148
  missing.env.length === 0 &&
147
149
  missing.config.length === 0 &&
148
150
  missing.os.length === 0));
149
- const preloaded = bundledSkillNames
150
- ? bundledSkillNames.has(entry.skill.name)
151
- : false;
151
+ const preloaded = bundledSkillNames ? bundledSkillNames.has(entry.skill.name) : false;
152
+ const isMarketplace = hasMarker(entry.skill.baseDir);
153
+ let marketplaceVersion;
154
+ if (isMarketplace) {
155
+ try {
156
+ const markerContent = fs.readFileSync(path.join(entry.skill.baseDir, ".skillpack"), "utf-8");
157
+ const marker = JSON.parse(markerContent);
158
+ marketplaceVersion = typeof marker.version === "string" ? marker.version : undefined;
159
+ }
160
+ catch {
161
+ // Marker exists but unreadable
162
+ }
163
+ }
152
164
  return {
153
165
  name: entry.skill.name,
154
166
  description: entry.skill.description,
@@ -160,10 +172,13 @@ function buildSkillStatus(entry, config, prefs, eligibility, bundledSkillNames)
160
172
  emoji,
161
173
  homepage,
162
174
  always,
175
+ alwaysActive,
163
176
  disabled,
164
177
  blockedByAllowlist,
165
178
  eligible,
166
179
  preloaded,
180
+ marketplace: isMarketplace,
181
+ marketplaceVersion,
167
182
  requirements: {
168
183
  bins: requiredBins,
169
184
  anyBins: requiredAnyBins,
@@ -8,11 +8,12 @@ function buildSkillsSection(params) {
8
8
  return [];
9
9
  return [
10
10
  "## Skills (mandatory)",
11
- "Before taking any action (including tool calls): scan <available_skills> <description> entries.",
12
- `- If exactly one skill clearly applies: read its SKILL.md at <location> with \`${params.readToolName}\`, then follow it.`,
11
+ "Before taking any action (including tool calls): scan ALL <available_skills> <description> entries for relevance.",
12
+ `- If one or more skills could apply (even partially): read the best match SKILL.md at <location> with \`${params.readToolName}\`, then follow it.`,
13
13
  "- If multiple could apply: choose the most specific one, then read/follow it.",
14
- "- If none clearly apply: do not read any SKILL.md.",
15
- "Skills encode expertise that improves output quality. Skipping them produces worse results even when the task seems simple.",
14
+ "- Only skip skills when the question is clearly unrelated to every skill description.",
15
+ "- When in doubt, read the skill the cost of reading is low; the cost of missing relevant knowledge is high.",
16
+ "Skills encode expertise and knowledge sources that you do not have in your training data. Skipping them produces worse results even when the task seems simple.",
16
17
  "Constraints: never read more than one skill up front; only read after selecting.",
17
18
  trimmed,
18
19
  "",
@@ -28,6 +29,7 @@ function buildMemorySection(params) {
28
29
  "## Memory Recall",
29
30
  "Memory is your knowledge base — customer profiles, business data, preferences, prior interactions, lessons, and instructions all live there. Proactively use memory_search whenever a topic might have stored context: who a person is, what was discussed before, business rules, pricing, product details, or anything that could have been recorded. Do not rely on conversation history alone — it only covers the current session. Use memory_get to pull specific lines when you know the file. If a search returns no results, say you checked.",
30
31
  "Paths for memory_get and memory_write accept either form: `memory/public/data.md` or `public/data.md` — the `memory/` prefix is added automatically when missing. Never use read or skill_read for memory content.",
32
+ "If memory_search returns no useful results for a technical or platform question, check your skills — a skill may provide an alternative knowledge source (e.g. live documentation) before you give up.",
31
33
  "",
32
34
  ];
33
35
  }
@@ -24,6 +24,8 @@ import { createCurrentTimeTool } from "./tools/current-time.js";
24
24
  import { createAuthorizeAdminTool, createListAdminsTool, createRevokeAdminTool, } from "./tools/authorize-admin-tool.js";
25
25
  import { createBootstrapCompleteTool } from "./tools/bootstrap-tool.js";
26
26
  import { createLicenseGenerateTool } from "./tools/license-tool.js";
27
+ import { createSkillPackSignTool } from "./tools/skill-pack-sign-tool.js";
28
+ import { createSkillPackInstallTool } from "./tools/skill-pack-install-tool.js";
27
29
  import { createContactCreateTool } from "./tools/contact-create-tool.js";
28
30
  import { createContactDeleteTool } from "./tools/contact-delete-tool.js";
29
31
  import { createFileDeleteTool } from "./tools/file-delete-tool.js";
@@ -158,6 +160,8 @@ export function createTaskmasterTools(options) {
158
160
  createListAdminsTool({ agentAccountId: options?.agentAccountId }),
159
161
  createBootstrapCompleteTool({ agentSessionKey: options?.agentSessionKey }),
160
162
  createLicenseGenerateTool(),
163
+ createSkillPackSignTool({ workspaceDir: options?.workspaceDir ?? "" }),
164
+ createSkillPackInstallTool({ workspaceDir: options?.workspaceDir ?? "" }),
161
165
  createContactCreateTool({ agentAccountId: options?.agentAccountId }),
162
166
  createContactDeleteTool({ agentAccountId: options?.agentAccountId }),
163
167
  createContactLookupTool({ agentAccountId: options?.agentAccountId }),
@@ -34,7 +34,7 @@ export const TOOL_GROUPS = {
34
34
  // Nodes + device tools
35
35
  "group:nodes": ["nodes"],
36
36
  // Skill management
37
- "group:skills": ["skill_read", "skill_draft_save"],
37
+ "group:skills": ["skill_read", "skill_draft_save", "skill_pack_install"],
38
38
  // Contact record management
39
39
  "group:contacts": [
40
40
  "contact_create",
@@ -104,6 +104,7 @@ export const TOOL_GROUPS = {
104
104
  "logs_read",
105
105
  "network_settings",
106
106
  "wifi_settings",
107
+ "skill_pack_install",
107
108
  ],
108
109
  };
109
110
  // Tools that are never granted by profiles — must be explicitly added to the
@@ -0,0 +1,90 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { jsonResult, readStringParam } from "./common.js";
5
+ import { verifySkillPack, hasMarker, readMarker, writeMarker, } from "../../license/skill-pack.js";
6
+ import { getDeviceId } from "../../license/device-id.js";
7
+ import { callGatewayTool } from "./gateway.js";
8
+ const SkillPackInstallSchema = Type.Object({
9
+ filePath: Type.String({
10
+ description: "Absolute path to the .skillpack.json file received as an attachment.",
11
+ }),
12
+ });
13
+ export function createSkillPackInstallTool(opts) {
14
+ return {
15
+ label: "Skill Pack Install",
16
+ name: "skill_pack_install",
17
+ description: "Install a signed skill pack (.skillpack.json) onto this device. " +
18
+ "Verifies the cryptographic signature and device binding before installing. " +
19
+ "Use this when a customer forwards a skill pack file to you.",
20
+ parameters: SkillPackInstallSchema,
21
+ execute: async (_toolCallId, args) => {
22
+ const params = args;
23
+ const filePath = readStringParam(params, "filePath", { required: true });
24
+ // Read and parse the pack file
25
+ let raw;
26
+ try {
27
+ const fileContent = await fsp.readFile(filePath, "utf-8");
28
+ raw = JSON.parse(fileContent);
29
+ }
30
+ catch {
31
+ throw new Error("Could not read or parse the skill pack file. " +
32
+ "Make sure the file path is correct and the file is valid JSON.");
33
+ }
34
+ // Verify signature and structure
35
+ const result = verifySkillPack(raw);
36
+ if (!result.valid) {
37
+ throw new Error(result.message);
38
+ }
39
+ const { payload } = result;
40
+ // Check device binding
41
+ const localDeviceId = getDeviceId();
42
+ if (payload.device.did !== "*" && payload.device.did !== localDeviceId) {
43
+ throw new Error("This skill pack is licensed for a different device. " +
44
+ "It cannot be installed on this device.");
45
+ }
46
+ // Check for existing marketplace skill
47
+ const skillDir = path.join(opts.workspaceDir, "skills", payload.pack.id);
48
+ if (hasMarker(skillDir)) {
49
+ const existing = await readMarker(skillDir);
50
+ if (existing && existing.version === payload.pack.version) {
51
+ return jsonResult({
52
+ ok: true,
53
+ alreadyInstalled: true,
54
+ packId: payload.pack.id,
55
+ version: payload.pack.version,
56
+ message: `${payload.pack.name} v${payload.pack.version} is already installed.`,
57
+ });
58
+ }
59
+ }
60
+ // Install via skills.create gateway RPC
61
+ const references = payload.content.references.map((r) => ({
62
+ name: r.name,
63
+ content: r.content,
64
+ }));
65
+ await callGatewayTool("skills.create", {}, {
66
+ name: payload.pack.id,
67
+ skillContent: payload.content.skill,
68
+ ...(references.length > 0 ? { references } : {}),
69
+ });
70
+ // Write marker file
71
+ const marker = {
72
+ packId: payload.pack.id,
73
+ version: payload.pack.version,
74
+ author: payload.pack.author,
75
+ installedAt: new Date().toISOString(),
76
+ deviceId: payload.device.did,
77
+ customerId: payload.device.cid,
78
+ };
79
+ await writeMarker(skillDir, marker);
80
+ return jsonResult({
81
+ ok: true,
82
+ packId: payload.pack.id,
83
+ version: payload.pack.version,
84
+ name: payload.pack.name,
85
+ message: `Successfully installed ${payload.pack.name} v${payload.pack.version}. ` +
86
+ `This skill is now available for use.`,
87
+ });
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,91 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { jsonResult, readStringParam } from "./common.js";
5
+ import { signSkillPack } from "../../license/skill-pack.js";
6
+ import { parseFrontmatter } from "../skills/frontmatter.js";
7
+ const SkillPackSignSchema = Type.Object({
8
+ packId: Type.String({
9
+ description: "Skill pack ID — must match a directory name in the skill-packs/ folder of this workspace.",
10
+ }),
11
+ deviceId: Type.String({
12
+ description: "Target device ID (tm_dev_...) from the customer's contact record.",
13
+ }),
14
+ customerId: Type.String({
15
+ description: "Customer identifier from the contact record.",
16
+ }),
17
+ version: Type.Optional(Type.String({ description: 'Skill pack version (e.g. "1.0.0"). Defaults to "1.0.0".' })),
18
+ });
19
+ export function createSkillPackSignTool(opts) {
20
+ return {
21
+ label: "Skill Pack Sign",
22
+ name: "skill_pack_sign",
23
+ description: "Sign a skill pack for a specific customer device. Reads the skill template from " +
24
+ "skill-packs/<packId>/, signs it with the customer's device ID embedded, and writes " +
25
+ "the signed .skillpack.json file. Send the resulting file to the customer.",
26
+ parameters: SkillPackSignSchema,
27
+ execute: async (_toolCallId, args) => {
28
+ const params = args;
29
+ const packId = readStringParam(params, "packId", { required: true });
30
+ const deviceId = readStringParam(params, "deviceId", { required: true });
31
+ const customerId = readStringParam(params, "customerId", { required: true });
32
+ const version = readStringParam(params, "version") ?? "1.0.0";
33
+ if (!deviceId.startsWith("tm_dev_")) {
34
+ throw new Error(`Invalid device ID format: "${deviceId}". Device IDs start with "tm_dev_".`);
35
+ }
36
+ // Read skill template
37
+ const packDir = path.join(opts.workspaceDir, "skill-packs", packId);
38
+ const skillPath = path.join(packDir, "SKILL.md");
39
+ let skillContent;
40
+ try {
41
+ skillContent = await fsp.readFile(skillPath, "utf-8");
42
+ }
43
+ catch {
44
+ throw new Error(`Skill pack template not found: ${packId}. Expected SKILL.md at ${skillPath}`);
45
+ }
46
+ // Read references
47
+ const refsDir = path.join(packDir, "references");
48
+ const references = [];
49
+ try {
50
+ const refFiles = await fsp.readdir(refsDir);
51
+ for (const file of refFiles.sort()) {
52
+ const content = await fsp.readFile(path.join(refsDir, file), "utf-8");
53
+ references.push({ name: file, content });
54
+ }
55
+ }
56
+ catch {
57
+ // No references directory — that's fine
58
+ }
59
+ // Parse frontmatter for metadata
60
+ const frontmatter = parseFrontmatter(skillContent);
61
+ const name = frontmatter?.name ?? packId;
62
+ const description = frontmatter?.description ?? "";
63
+ // Build and sign payload
64
+ const payload = {
65
+ format: "skillpack-v1",
66
+ pack: { id: packId, version, name, description, author: "Rubytech LLC" },
67
+ device: { did: deviceId, cid: customerId },
68
+ content: { skill: skillContent, references },
69
+ signedAt: new Date().toISOString(),
70
+ };
71
+ const signed = signSkillPack(payload);
72
+ // Write signed file
73
+ const shortDid = deviceId.slice(0, 20);
74
+ const signedDir = path.join(opts.workspaceDir, "skill-packs", "signed");
75
+ await fsp.mkdir(signedDir, { recursive: true });
76
+ const outputPath = path.join(signedDir, `${packId}-${shortDid}.skillpack.json`);
77
+ await fsp.writeFile(outputPath, JSON.stringify(signed, null, 2), "utf-8");
78
+ return jsonResult({
79
+ ok: true,
80
+ filePath: outputPath,
81
+ packId,
82
+ version,
83
+ deviceId,
84
+ customerId,
85
+ message: `Signed skill pack "${name}" v${version} for device ${deviceId}. ` +
86
+ `File saved to ${outputPath}. Send this file to the customer with instructions ` +
87
+ `to forward it to their admin agent and ask it to install.`,
88
+ });
89
+ },
90
+ };
91
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.19.1",
3
- "commit": "30511004b7e6a6d7a2e6bc573cacd5eedf07620c",
4
- "builtAt": "2026-03-06T14:26:40.923Z"
2
+ "version": "1.20.0",
3
+ "commit": "2e49941c12d70d4e6618e5d40e5291beb58c0420",
4
+ "builtAt": "2026-03-06T19:10:34.821Z"
5
5
  }