@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
|
@@ -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
|
-
|
|
151
|
-
|
|
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
|
|
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
|
-
"-
|
|
15
|
-
"
|
|
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
|
+
}
|
package/dist/build-info.json
CHANGED