Skip to content

Commit 202af61

Browse files
committed
feat(sdk): add skills.define + chat.skills runtime wiring
skills.define({ id, path }) registers a skill with the resource catalog and returns a SkillHandle. SkillHandle.local() reads the bundled SKILL.md from ./.trigger/skills/{id}/ at runtime, parses frontmatter, and returns a ResolvedSkill ready for chat.skills.set(). chat.skills.set([...]) stores resolved skills for the current run. chat.toStreamTextOptions() auto-injects the skills preamble into the system prompt and merges three tools — loadSkill, readFile, bash — scoped per-skill with path-traversal guards and output caps (64 KB stdout/stderr, 1 MB readFile). Bash executes in the worker container with the turn's abort signal, no sandbox — skills are developer code. Shared packages/build/src/internal/copyFiles.ts extracted from the additionalFiles extension so the CLI's built-in skill bundler and the existing extension share one glob + copy implementation. Part 2/3 of Phase 1 for the new ai.skills primitive.
1 parent f09bd1b commit 202af61

9 files changed

Lines changed: 890 additions & 85 deletions

File tree

packages/build/src/internal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./internal/additionalFiles.js";
2+
export * from "./internal/copyFiles.js";
Lines changed: 13 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { BuildManifest } from "@trigger.dev/core/v3";
22
import { BuildContext } from "@trigger.dev/core/v3/build";
3-
import { copyFile, mkdir } from "node:fs/promises";
4-
import { dirname, join, posix, relative } from "node:path";
5-
import { glob } from "tinyglobby";
3+
import {
4+
copyMatcherResults,
5+
findFilesByMatchers,
6+
type MatcherResult,
7+
} from "./copyFiles.js";
68

79
export type AdditionalFilesOptions = {
810
files: string[];
@@ -14,93 +16,21 @@ export async function addAdditionalFilesToBuild(
1416
context: BuildContext,
1517
manifest: BuildManifest
1618
) {
17-
// Copy any static assets to the destination
18-
const staticAssets = await findStaticAssetFiles(options.files ?? [], manifest.outputPath, {
19-
cwd: context.workingDir,
20-
});
19+
const matcherResults: MatcherResult[] = await findFilesByMatchers(
20+
options.files ?? [],
21+
manifest.outputPath,
22+
{ cwd: context.workingDir }
23+
);
2124

22-
for (const { assets, matcher } of staticAssets) {
25+
for (const { assets, matcher } of matcherResults) {
2326
if (assets.length === 0) {
2427
context.logger.warn(`[${source}] No files found for matcher`, matcher);
2528
} else {
2629
context.logger.debug(`[${source}] Found ${assets.length} files for matcher`, matcher);
2730
}
2831
}
2932

30-
await copyStaticAssets(staticAssets, source, context);
31-
}
32-
33-
type MatchedStaticAssets = { source: string; destination: string }[];
34-
35-
type FoundStaticAssetFiles = Array<{
36-
matcher: string;
37-
assets: MatchedStaticAssets;
38-
}>;
39-
40-
async function findStaticAssetFiles(
41-
matchers: string[],
42-
destinationPath: string,
43-
options?: { cwd?: string; ignore?: string[] }
44-
): Promise<FoundStaticAssetFiles> {
45-
const result: FoundStaticAssetFiles = [];
46-
47-
for (const matcher of matchers) {
48-
const assets = await findStaticAssetsForMatcher(matcher, destinationPath, options);
49-
50-
result.push({ matcher, assets });
51-
}
52-
53-
return result;
54-
}
55-
56-
async function findStaticAssetsForMatcher(
57-
matcher: string,
58-
destinationPath: string,
59-
options?: { cwd?: string; ignore?: string[] }
60-
): Promise<MatchedStaticAssets> {
61-
const result: MatchedStaticAssets = [];
62-
63-
const files = await glob({
64-
patterns: [matcher],
65-
cwd: options?.cwd,
66-
ignore: options?.ignore ?? [],
67-
onlyFiles: true,
68-
absolute: true,
33+
await copyMatcherResults(matcherResults, (pair) => {
34+
context.logger.debug(`[${source}] Copying ${pair.source} to ${pair.destination}`);
6935
});
70-
71-
let matches = 0;
72-
73-
for (const file of files) {
74-
matches++;
75-
76-
const pathInsideDestinationDir = relative(options?.cwd ?? process.cwd(), file)
77-
.split(posix.sep)
78-
.filter((p) => p !== "..")
79-
.join(posix.sep);
80-
81-
const relativeDestinationPath = join(destinationPath, pathInsideDestinationDir);
82-
83-
result.push({
84-
source: file,
85-
destination: relativeDestinationPath,
86-
});
87-
}
88-
89-
return result;
90-
}
91-
92-
async function copyStaticAssets(
93-
staticAssetFiles: FoundStaticAssetFiles,
94-
sourceName: string,
95-
context: BuildContext
96-
): Promise<void> {
97-
for (const { assets } of staticAssetFiles) {
98-
for (const { source, destination } of assets) {
99-
await mkdir(dirname(destination), { recursive: true });
100-
101-
context.logger.debug(`[${sourceName}] Copying ${source} to ${destination}`);
102-
103-
await copyFile(source, destination);
104-
}
105-
}
10636
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { cp, copyFile, mkdir } from "node:fs/promises";
2+
import { dirname, join, posix, relative } from "node:path";
3+
import { glob } from "tinyglobby";
4+
5+
/**
6+
* A single matched asset — source file and its destination inside the
7+
* build output directory.
8+
*/
9+
export type CopyPair = { source: string; destination: string };
10+
11+
/**
12+
* Result of a single matcher's glob, grouped with the matcher that
13+
* produced it so callers can warn on empty matches.
14+
*/
15+
export type MatcherResult = {
16+
matcher: string;
17+
assets: CopyPair[];
18+
};
19+
20+
/**
21+
* Glob a set of matchers relative to `cwd` and return pairs describing
22+
* where each matched file should be copied to under `destinationDir`.
23+
*
24+
* Relative paths are preserved under `destinationDir`. Leading `..`
25+
* segments (from `../shared/file.txt` style patterns) are stripped so
26+
* files always land inside the destination.
27+
*/
28+
export async function findFilesByMatchers(
29+
matchers: string[],
30+
destinationDir: string,
31+
options?: { cwd?: string; ignore?: string[] }
32+
): Promise<MatcherResult[]> {
33+
const result: MatcherResult[] = [];
34+
const cwd = options?.cwd ?? process.cwd();
35+
36+
for (const matcher of matchers) {
37+
const files = await glob({
38+
patterns: [matcher],
39+
cwd,
40+
ignore: options?.ignore ?? [],
41+
onlyFiles: true,
42+
absolute: true,
43+
});
44+
45+
const assets: CopyPair[] = files.map((file) => {
46+
const pathInsideDestinationDir = relative(cwd, file)
47+
.split(posix.sep)
48+
.filter((p) => p !== "..")
49+
.join(posix.sep);
50+
return {
51+
source: file,
52+
destination: join(destinationDir, pathInsideDestinationDir),
53+
};
54+
});
55+
56+
result.push({ matcher, assets });
57+
}
58+
59+
return result;
60+
}
61+
62+
/**
63+
* Copy a single file, creating parent directories as needed.
64+
*/
65+
export async function copyFileEnsuringDir(source: string, destination: string): Promise<void> {
66+
await mkdir(dirname(destination), { recursive: true });
67+
await copyFile(source, destination);
68+
}
69+
70+
/**
71+
* Copy every pair in the given matcher results. Parent directories are
72+
* created automatically. Returns the total number of files copied.
73+
*/
74+
export async function copyMatcherResults(
75+
matcherResults: MatcherResult[],
76+
onCopy?: (pair: CopyPair) => void
77+
): Promise<number> {
78+
let count = 0;
79+
for (const { assets } of matcherResults) {
80+
for (const pair of assets) {
81+
onCopy?.(pair);
82+
await copyFileEnsuringDir(pair.source, pair.destination);
83+
count++;
84+
}
85+
}
86+
return count;
87+
}
88+
89+
/**
90+
* Recursively copy a directory to another location. Preserves structure;
91+
* overwrites existing files at the destination.
92+
*
93+
* Used by the built-in skill bundler — we copy entire skill folders as a
94+
* unit, not file-by-file.
95+
*/
96+
export async function copyDirectoryRecursive(source: string, destination: string): Promise<void> {
97+
await mkdir(destination, { recursive: true });
98+
await cp(source, destination, { recursive: true, force: true });
99+
}

0 commit comments

Comments
 (0)