@poncho-ai/harness 0.2.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.
@@ -0,0 +1,357 @@
1
+ import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
2
+ import { access, readdir } from "node:fs/promises";
3
+ import { extname, normalize, resolve, sep } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { createJiti } from "jiti";
6
+ import type { SkillMetadata } from "./skill-context.js";
7
+ import { loadSkillInstructions, readSkillResource } from "./skill-context.js";
8
+
9
+ /**
10
+ * Creates the built-in skill tools that implement progressive disclosure
11
+ * per the Agent Skills specification (https://agentskills.io/integrate-skills).
12
+ *
13
+ * - `activate_skill` — loads the full SKILL.md body on demand
14
+ * - `read_skill_resource` — reads a file from a skill directory (references, scripts, assets)
15
+ * - `list_skill_scripts` — lists runnable JavaScript/TypeScript scripts under scripts/
16
+ * - `run_skill_script` — executes a JavaScript/TypeScript module under scripts/
17
+ */
18
+ export const createSkillTools = (
19
+ skills: SkillMetadata[],
20
+ ): ToolDefinition[] => {
21
+ if (skills.length === 0) {
22
+ return [];
23
+ }
24
+
25
+ const skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
26
+ const knownNames = skills.map((skill) => skill.name).join(", ");
27
+
28
+ return [
29
+ defineTool({
30
+ name: "activate_skill",
31
+ description:
32
+ "Load the full instructions for an available skill. " +
33
+ "Use this when a user's request matches a skill's description. " +
34
+ `Available skills: ${knownNames}`,
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {
38
+ name: {
39
+ type: "string",
40
+ description: "Name of the skill to activate",
41
+ },
42
+ },
43
+ required: ["name"],
44
+ additionalProperties: false,
45
+ },
46
+ handler: async (input) => {
47
+ const name = typeof input.name === "string" ? input.name.trim() : "";
48
+ const skill = skillsByName.get(name);
49
+ if (!skill) {
50
+ return {
51
+ error: `Unknown skill: "${name}". Available skills: ${knownNames}`,
52
+ };
53
+ }
54
+ try {
55
+ const instructions = await loadSkillInstructions(skill);
56
+ return {
57
+ skill: name,
58
+ instructions: instructions || "(no instructions provided)",
59
+ };
60
+ } catch (err) {
61
+ return {
62
+ error: `Failed to load skill "${name}": ${err instanceof Error ? err.message : String(err)}`,
63
+ };
64
+ }
65
+ },
66
+ }),
67
+ defineTool({
68
+ name: "read_skill_resource",
69
+ description:
70
+ "Read a file from a skill's directory (references, scripts, assets). " +
71
+ "Use relative paths from the skill root. " +
72
+ `Available skills: ${knownNames}`,
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ skill: {
77
+ type: "string",
78
+ description: "Name of the skill",
79
+ },
80
+ path: {
81
+ type: "string",
82
+ description:
83
+ "Relative path to the file within the skill directory (e.g. references/REFERENCE.md)",
84
+ },
85
+ },
86
+ required: ["skill", "path"],
87
+ additionalProperties: false,
88
+ },
89
+ handler: async (input) => {
90
+ const name = typeof input.skill === "string" ? input.skill.trim() : "";
91
+ const path = typeof input.path === "string" ? input.path.trim() : "";
92
+ const skill = skillsByName.get(name);
93
+ if (!skill) {
94
+ return {
95
+ error: `Unknown skill: "${name}". Available skills: ${knownNames}`,
96
+ };
97
+ }
98
+ if (!path) {
99
+ return { error: "Path is required" };
100
+ }
101
+ try {
102
+ const content = await readSkillResource(skill, path);
103
+ return { skill: name, path, content };
104
+ } catch (err) {
105
+ return {
106
+ error: `Failed to read "${path}" from skill "${name}": ${err instanceof Error ? err.message : String(err)}`,
107
+ };
108
+ }
109
+ },
110
+ }),
111
+ defineTool({
112
+ name: "list_skill_scripts",
113
+ description:
114
+ "List JavaScript/TypeScript script files available under a skill's scripts directory. " +
115
+ `Available skills: ${knownNames}`,
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {
119
+ skill: {
120
+ type: "string",
121
+ description: "Name of the skill",
122
+ },
123
+ },
124
+ required: ["skill"],
125
+ additionalProperties: false,
126
+ },
127
+ handler: async (input) => {
128
+ const name = typeof input.skill === "string" ? input.skill.trim() : "";
129
+ const skill = skillsByName.get(name);
130
+ if (!skill) {
131
+ return {
132
+ error: `Unknown skill: "${name}". Available skills: ${knownNames}`,
133
+ };
134
+ }
135
+ try {
136
+ const scripts = await listSkillScripts(skill);
137
+ return {
138
+ skill: name,
139
+ scripts,
140
+ };
141
+ } catch (err) {
142
+ return {
143
+ error: `Failed to list scripts for skill "${name}": ${err instanceof Error ? err.message : String(err)}`,
144
+ };
145
+ }
146
+ },
147
+ }),
148
+ defineTool({
149
+ name: "run_skill_script",
150
+ description:
151
+ "Run a JavaScript/TypeScript module in a skill's scripts directory. " +
152
+ "Uses default export function or named run/main/handler function. " +
153
+ `Available skills: ${knownNames}`,
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ skill: {
158
+ type: "string",
159
+ description: "Name of the skill",
160
+ },
161
+ script: {
162
+ type: "string",
163
+ description:
164
+ "Relative path under scripts/ (e.g. scripts/summarize.ts or summarize.ts)",
165
+ },
166
+ input: {
167
+ type: "object",
168
+ description: "Optional JSON input payload passed to the script function",
169
+ },
170
+ },
171
+ required: ["skill", "script"],
172
+ additionalProperties: false,
173
+ },
174
+ handler: async (input) => {
175
+ const name = typeof input.skill === "string" ? input.skill.trim() : "";
176
+ const script = typeof input.script === "string" ? input.script.trim() : "";
177
+ const payload =
178
+ typeof input.input === "object" && input.input !== null
179
+ ? (input.input as Record<string, unknown>)
180
+ : {};
181
+
182
+ const skill = skillsByName.get(name);
183
+ if (!skill) {
184
+ return {
185
+ error: `Unknown skill: "${name}". Available skills: ${knownNames}`,
186
+ };
187
+ }
188
+ if (!script) {
189
+ return { error: "Script path is required" };
190
+ }
191
+
192
+ try {
193
+ const scriptPath = resolveSkillScriptPath(skill, script);
194
+ await access(scriptPath);
195
+ const fn = await loadRunnableScriptFunction(scriptPath);
196
+ const output = await fn(payload, {
197
+ skill: name,
198
+ skillDir: skill.skillDir,
199
+ scriptPath,
200
+ });
201
+ return {
202
+ skill: name,
203
+ script,
204
+ output,
205
+ };
206
+ } catch (err) {
207
+ return {
208
+ error: `Failed to run script "${script}" in skill "${name}": ${err instanceof Error ? err.message : String(err)}`,
209
+ };
210
+ }
211
+ },
212
+ }),
213
+ ];
214
+ };
215
+
216
+ const SCRIPT_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]);
217
+
218
+ const listSkillScripts = async (skill: SkillMetadata): Promise<string[]> => {
219
+ const scriptsRoot = resolve(skill.skillDir, "scripts");
220
+ try {
221
+ await access(scriptsRoot);
222
+ } catch {
223
+ return [];
224
+ }
225
+
226
+ const scripts = await collectScriptFiles(scriptsRoot);
227
+ return scripts
228
+ .map((fullPath) => `scripts/${fullPath.slice(scriptsRoot.length + 1).split(sep).join("/")}`)
229
+ .sort();
230
+ };
231
+
232
+ const collectScriptFiles = async (directory: string): Promise<string[]> => {
233
+ const entries = await readdir(directory, { withFileTypes: true });
234
+ const files: string[] = [];
235
+
236
+ for (const entry of entries) {
237
+ const fullPath = resolve(directory, entry.name);
238
+ if (entry.isDirectory()) {
239
+ files.push(...(await collectScriptFiles(fullPath)));
240
+ continue;
241
+ }
242
+ if (entry.isFile()) {
243
+ const extension = extname(fullPath).toLowerCase();
244
+ if (SCRIPT_EXTENSIONS.has(extension)) {
245
+ files.push(fullPath);
246
+ }
247
+ }
248
+ }
249
+ return files;
250
+ };
251
+
252
+ const resolveSkillScriptPath = (skill: SkillMetadata, relativePath: string): string => {
253
+ const normalized = normalize(relativePath);
254
+ if (normalized.startsWith("..") || normalized.startsWith("/")) {
255
+ throw new Error("Script path must be relative and within the skill directory");
256
+ }
257
+
258
+ const normalizedWithPrefix = normalized.startsWith("scripts/")
259
+ ? normalized
260
+ : `scripts/${normalized}`;
261
+ const fullPath = resolve(skill.skillDir, normalizedWithPrefix);
262
+ const scriptsRoot = resolve(skill.skillDir, "scripts");
263
+
264
+ if (!fullPath.startsWith(`${scriptsRoot}${sep}`) && fullPath !== scriptsRoot) {
265
+ throw new Error("Script path must stay inside the scripts directory");
266
+ }
267
+
268
+ const extension = extname(fullPath).toLowerCase();
269
+ if (!SCRIPT_EXTENSIONS.has(extension)) {
270
+ throw new Error(
271
+ `Unsupported script extension "${extension || "(none)"}". Allowed: ${[...SCRIPT_EXTENSIONS].join(", ")}`,
272
+ );
273
+ }
274
+
275
+ return fullPath;
276
+ };
277
+
278
+ type RunnableScriptFunction = (
279
+ input: Record<string, unknown>,
280
+ context: {
281
+ skill: string;
282
+ skillDir: string;
283
+ scriptPath: string;
284
+ },
285
+ ) => unknown | Promise<unknown>;
286
+
287
+ const loadRunnableScriptFunction = async (
288
+ scriptPath: string,
289
+ ): Promise<RunnableScriptFunction> => {
290
+ const loaded = await loadScriptModule(scriptPath);
291
+ const fn = extractRunnableFunction(loaded);
292
+ if (!fn) {
293
+ throw new Error(
294
+ "Script module must export a function (default export or named run/main/handler)",
295
+ );
296
+ }
297
+ return fn;
298
+ };
299
+
300
+ const loadScriptModule = async (scriptPath: string): Promise<unknown> => {
301
+ try {
302
+ return await import(pathToFileURL(scriptPath).href);
303
+ } catch {
304
+ const jiti = createJiti(import.meta.url, { interopDefault: true });
305
+ return await jiti.import(scriptPath);
306
+ }
307
+ };
308
+
309
+ const extractRunnableFunction = (value: unknown): RunnableScriptFunction | undefined => {
310
+ if (typeof value === "function") {
311
+ return value as RunnableScriptFunction;
312
+ }
313
+ if (Array.isArray(value) || typeof value !== "object" || value === null) {
314
+ return undefined;
315
+ }
316
+
317
+ const module = value as {
318
+ default?: unknown;
319
+ run?: unknown;
320
+ main?: unknown;
321
+ handler?: unknown;
322
+ };
323
+ const defaultValue = module.default;
324
+ if (typeof defaultValue === "function") {
325
+ return defaultValue as RunnableScriptFunction;
326
+ }
327
+ if (typeof module.run === "function") {
328
+ return module.run as RunnableScriptFunction;
329
+ }
330
+ if (typeof module.main === "function") {
331
+ return module.main as RunnableScriptFunction;
332
+ }
333
+ if (typeof module.handler === "function") {
334
+ return module.handler as RunnableScriptFunction;
335
+ }
336
+ if (
337
+ defaultValue &&
338
+ typeof defaultValue === "object" &&
339
+ !Array.isArray(defaultValue)
340
+ ) {
341
+ const inner = defaultValue as {
342
+ run?: unknown;
343
+ main?: unknown;
344
+ handler?: unknown;
345
+ };
346
+ if (typeof inner.run === "function") {
347
+ return inner.run as RunnableScriptFunction;
348
+ }
349
+ if (typeof inner.main === "function") {
350
+ return inner.main as RunnableScriptFunction;
351
+ }
352
+ if (typeof inner.handler === "function") {
353
+ return inner.handler as RunnableScriptFunction;
354
+ }
355
+ }
356
+ return undefined;
357
+ };