@rubytech/taskmaster 1.19.8 → 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.
@@ -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;
@@ -148,6 +149,18 @@ function buildSkillStatus(entry, config, prefs, eligibility, bundledSkillNames)
148
149
  missing.config.length === 0 &&
149
150
  missing.os.length === 0));
150
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
+ }
151
164
  return {
152
165
  name: entry.skill.name,
153
166
  description: entry.skill.description,
@@ -164,6 +177,8 @@ function buildSkillStatus(entry, config, prefs, eligibility, bundledSkillNames)
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,
@@ -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.8",
3
- "commit": "2776d5020a5836fa4ff5de337d0a4ba51e239d95",
4
- "builtAt": "2026-03-06T16:01:44.873Z"
2
+ "version": "1.20.0",
3
+ "commit": "2e49941c12d70d4e6618e5d40e5291beb58c0420",
4
+ "builtAt": "2026-03-06T19:10:34.821Z"
5
5
  }