veryfront 0.1.434 → 0.1.436
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/esm/deno.js +1 -1
- package/esm/src/agent/index.d.ts +3 -1
- package/esm/src/agent/index.d.ts.map +1 -1
- package/esm/src/agent/index.js +3 -1
- package/esm/src/agent/runtime/index.d.ts +1 -0
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +1 -0
- package/esm/src/agent/runtime/model-tool-converter.d.ts.map +1 -1
- package/esm/src/agent/runtime/model-tool-converter.js +6 -2
- package/esm/src/agent/runtime/provider-tool-compat.d.ts +17 -0
- package/esm/src/agent/runtime/provider-tool-compat.d.ts.map +1 -0
- package/esm/src/agent/runtime/provider-tool-compat.js +177 -0
- package/esm/src/agent/runtime-project-skill-catalog.d.ts +26 -0
- package/esm/src/agent/runtime-project-skill-catalog.d.ts.map +1 -0
- package/esm/src/agent/runtime-project-skill-catalog.js +148 -0
- package/esm/src/agent/runtime-project-skill-loader.d.ts +28 -0
- package/esm/src/agent/runtime-project-skill-loader.d.ts.map +1 -0
- package/esm/src/agent/runtime-project-skill-loader.js +122 -0
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
- package/src/deno.js +1 -1
- package/src/deps/esm.sh/@types/react-dom@19.2.3/client.d.ts +1 -1
- package/src/deps/esm.sh/@types/{react@19.2.3 → react@19.2.14}/global.d.ts +1 -0
- package/src/deps/esm.sh/@types/{react@19.2.3 → react@19.2.14}/index.d.ts +93 -24
- package/src/deps/esm.sh/react-dom@19.2.4/client.d.ts +1 -1
- package/src/src/agent/index.ts +23 -0
- package/src/src/agent/runtime/index.ts +9 -0
- package/src/src/agent/runtime/model-tool-converter.ts +11 -2
- package/src/src/agent/runtime/provider-tool-compat.ts +239 -0
- package/src/src/agent/runtime-project-skill-catalog.ts +238 -0
- package/src/src/agent/runtime-project-skill-loader.ts +217 -0
- package/src/src/utils/version-constant.ts +1 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../../tool/index.js";
|
|
2
|
+
import type { JsonSchema } from "../../tool/schema/index.js";
|
|
3
|
+
|
|
4
|
+
export type ProviderToolCompatProvider =
|
|
5
|
+
| "anthropic"
|
|
6
|
+
| "google"
|
|
7
|
+
| "moonshot"
|
|
8
|
+
| "openai"
|
|
9
|
+
| "unknown";
|
|
10
|
+
|
|
11
|
+
export interface ProviderToolProfile {
|
|
12
|
+
provider: ProviderToolCompatProvider;
|
|
13
|
+
maxTools?: number;
|
|
14
|
+
sanitizeSchema: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ProviderToolCompatOptions {
|
|
18
|
+
model?: string;
|
|
19
|
+
requiredToolNames?: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const OPENAI_MAX_TOOLS = 128;
|
|
23
|
+
const GOOGLE_UNSUPPORTED_SCHEMA_KEYS = new Set([
|
|
24
|
+
"$id",
|
|
25
|
+
"$ref",
|
|
26
|
+
"$schema",
|
|
27
|
+
"additionalProperties",
|
|
28
|
+
"allOf",
|
|
29
|
+
"default",
|
|
30
|
+
"oneOf",
|
|
31
|
+
"prefixItems",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function normalizeModel(model?: string): string {
|
|
35
|
+
return model?.trim().toLowerCase() ?? "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getProviderToolProfile(model?: string): ProviderToolProfile {
|
|
39
|
+
const normalized = normalizeModel(model);
|
|
40
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
41
|
+
const provider = parts[0] === "veryfront-cloud" ? parts[1] : parts[0];
|
|
42
|
+
|
|
43
|
+
if (provider === "openai") {
|
|
44
|
+
return { provider: "openai", maxTools: OPENAI_MAX_TOOLS, sanitizeSchema: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (provider === "google" || provider === "google-ai-studio") {
|
|
48
|
+
return { provider: "google", sanitizeSchema: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (provider === "anthropic") {
|
|
52
|
+
return { provider: "anthropic", sanitizeSchema: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (provider === "moonshot") {
|
|
56
|
+
return { provider: "moonshot", sanitizeSchema: false };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { provider: "unknown", sanitizeSchema: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function uniqueInOrder(values: readonly string[]): string[] {
|
|
63
|
+
const seen = new Set<string>();
|
|
64
|
+
const result: string[] = [];
|
|
65
|
+
|
|
66
|
+
for (const value of values) {
|
|
67
|
+
if (seen.has(value)) continue;
|
|
68
|
+
seen.add(value);
|
|
69
|
+
result.push(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function selectProviderCompatibleToolNames(
|
|
76
|
+
toolNames: readonly string[],
|
|
77
|
+
options: ProviderToolCompatOptions = {},
|
|
78
|
+
): string[] {
|
|
79
|
+
const profile = getProviderToolProfile(options.model);
|
|
80
|
+
const orderedToolNames = uniqueInOrder(toolNames);
|
|
81
|
+
|
|
82
|
+
if (profile.maxTools === undefined || orderedToolNames.length <= profile.maxTools) {
|
|
83
|
+
return orderedToolNames;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const available = new Set(orderedToolNames);
|
|
87
|
+
const requiredToolNames = uniqueInOrder(options.requiredToolNames ?? [])
|
|
88
|
+
.filter((toolName) => available.has(toolName));
|
|
89
|
+
const selected = [...requiredToolNames];
|
|
90
|
+
const selectedSet = new Set(selected);
|
|
91
|
+
|
|
92
|
+
for (const toolName of orderedToolNames) {
|
|
93
|
+
if (selected.length >= profile.maxTools) break;
|
|
94
|
+
if (selectedSet.has(toolName)) continue;
|
|
95
|
+
selected.push(toolName);
|
|
96
|
+
selectedSet.add(toolName);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return selected.slice(0, profile.maxTools);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function selectProviderCompatibleTools(
|
|
103
|
+
tools: readonly ToolDefinition[],
|
|
104
|
+
options: ProviderToolCompatOptions = {},
|
|
105
|
+
): ToolDefinition[] {
|
|
106
|
+
const toolsByName = new Map<string, ToolDefinition>();
|
|
107
|
+
for (const tool of tools) {
|
|
108
|
+
if (!toolsByName.has(tool.name)) toolsByName.set(tool.name, tool);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const selectedToolNames = selectProviderCompatibleToolNames(
|
|
112
|
+
[...toolsByName.keys()],
|
|
113
|
+
options,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return selectedToolNames
|
|
117
|
+
.map((toolName) => toolsByName.get(toolName))
|
|
118
|
+
.filter((tool): tool is ToolDefinition => tool !== undefined);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
122
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getLiteralType(value: unknown): JsonSchema["type"] | undefined {
|
|
126
|
+
switch (typeof value) {
|
|
127
|
+
case "string":
|
|
128
|
+
return "string";
|
|
129
|
+
case "number":
|
|
130
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
131
|
+
case "boolean":
|
|
132
|
+
return "boolean";
|
|
133
|
+
default:
|
|
134
|
+
return value === null ? "null" : undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getEnumValuesFromAnyOf(anyOf: unknown): unknown[] | undefined {
|
|
139
|
+
if (!Array.isArray(anyOf)) return undefined;
|
|
140
|
+
|
|
141
|
+
const values: unknown[] = [];
|
|
142
|
+
for (const option of anyOf) {
|
|
143
|
+
if (!isPlainRecord(option)) return undefined;
|
|
144
|
+
if ("const" in option) {
|
|
145
|
+
values.push(option.const);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(option.enum) && option.enum.length > 0) {
|
|
149
|
+
values.push(...option.enum);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return values.length > 0 ? uniqueUnknownValues(values) : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function uniqueUnknownValues(values: unknown[]): unknown[] {
|
|
159
|
+
const result: unknown[] = [];
|
|
160
|
+
for (const value of values) {
|
|
161
|
+
if (result.some((existing) => Object.is(existing, value))) continue;
|
|
162
|
+
result.push(value);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getSharedLiteralType(values: readonly unknown[]): JsonSchema["type"] | undefined {
|
|
168
|
+
const literalTypes = values.map((value) => getLiteralType(value));
|
|
169
|
+
const firstType = literalTypes[0];
|
|
170
|
+
|
|
171
|
+
if (!firstType || literalTypes.some((literalType) => literalType !== firstType)) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return firstType;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sanitizeGoogleSchemaValue(value: unknown): unknown {
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
return value.map((item) => sanitizeGoogleSchemaValue(item));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!isPlainRecord(value)) {
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const sanitized: Record<string, unknown> = {};
|
|
188
|
+
const enumFromAnyOf = getEnumValuesFromAnyOf(value.anyOf);
|
|
189
|
+
const constValue = value.const;
|
|
190
|
+
|
|
191
|
+
for (const [key, child] of Object.entries(value)) {
|
|
192
|
+
if (key === "const" || key === "anyOf" || GOOGLE_UNSUPPORTED_SCHEMA_KEYS.has(key)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (key === "properties" && isPlainRecord(child)) {
|
|
197
|
+
sanitized.properties = Object.fromEntries(
|
|
198
|
+
Object.entries(child).map(([propertyName, propertySchema]) => [
|
|
199
|
+
propertyName,
|
|
200
|
+
sanitizeGoogleSchemaValue(propertySchema),
|
|
201
|
+
]),
|
|
202
|
+
);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (key === "items") {
|
|
207
|
+
sanitized.items = sanitizeGoogleSchemaValue(child);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
sanitized[key] = sanitizeGoogleSchemaValue(child);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (enumFromAnyOf) {
|
|
215
|
+
sanitized.enum = enumFromAnyOf;
|
|
216
|
+
if (!sanitized.type) {
|
|
217
|
+
const sharedType = getSharedLiteralType(enumFromAnyOf);
|
|
218
|
+
if (sharedType) sanitized.type = sharedType;
|
|
219
|
+
}
|
|
220
|
+
} else if ("const" in value) {
|
|
221
|
+
sanitized.enum = [constValue];
|
|
222
|
+
if (!sanitized.type) {
|
|
223
|
+
const literalType = getLiteralType(constValue);
|
|
224
|
+
if (literalType) sanitized.type = literalType;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return sanitized;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function sanitizeProviderToolSchema(
|
|
232
|
+
schema: JsonSchema,
|
|
233
|
+
options: Pick<ProviderToolCompatOptions, "model"> = {},
|
|
234
|
+
): JsonSchema {
|
|
235
|
+
const profile = getProviderToolProfile(options.model);
|
|
236
|
+
if (!profile.sanitizeSchema) return schema;
|
|
237
|
+
|
|
238
|
+
return sanitizeGoogleSchemaValue(schema) as JsonSchema;
|
|
239
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_PROJECT_STEERING_PATHS,
|
|
4
|
+
type ProjectSteeringPaths,
|
|
5
|
+
} from "./project-steering-mutation.js";
|
|
6
|
+
import type {
|
|
7
|
+
RuntimeGetProjectFileOptions,
|
|
8
|
+
RuntimeProjectFile,
|
|
9
|
+
RuntimeProjectFileListItem,
|
|
10
|
+
RuntimeProjectFilesApiOptions,
|
|
11
|
+
} from "./runtime-project-files-client.js";
|
|
12
|
+
import {
|
|
13
|
+
listRuntimeBuiltinSkillReferences,
|
|
14
|
+
readRuntimeBuiltinDirectorySkill,
|
|
15
|
+
readRuntimeBuiltinFlatSkill,
|
|
16
|
+
readRuntimeBuiltinSkillEntries,
|
|
17
|
+
} from "./runtime-builtin-skill-files.js";
|
|
18
|
+
import {
|
|
19
|
+
buildRuntimeSkillDefinition,
|
|
20
|
+
type RuntimeSkillDefinition,
|
|
21
|
+
type RuntimeSkillMetadataLogger,
|
|
22
|
+
} from "./runtime-skill-metadata.js";
|
|
23
|
+
|
|
24
|
+
export type RuntimeProjectSteeringLookup = {
|
|
25
|
+
projectId: string;
|
|
26
|
+
authToken: string;
|
|
27
|
+
branchId?: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RuntimeProjectSkillCatalogOptions = {
|
|
31
|
+
getProjectFile: (options: RuntimeGetProjectFileOptions) => Promise<RuntimeProjectFile | null>;
|
|
32
|
+
getProjectFiles: (
|
|
33
|
+
options: RuntimeProjectFilesApiOptions,
|
|
34
|
+
) => Promise<readonly RuntimeProjectFileListItem[] | null>;
|
|
35
|
+
builtinSkills: readonly RuntimeSkillDefinition[];
|
|
36
|
+
steeringPaths?: Pick<ProjectSteeringPaths, "skills">;
|
|
37
|
+
logger?: RuntimeSkillMetadataLogger;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type RuntimeProjectInstructionsOptions = {
|
|
41
|
+
getProjectFile: (options: RuntimeGetProjectFileOptions) => Promise<RuntimeProjectFile | null>;
|
|
42
|
+
steeringPaths?: Pick<ProjectSteeringPaths, "instructions">;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function sortSkillsById(skills: Iterable<RuntimeSkillDefinition>): RuntimeSkillDefinition[] {
|
|
46
|
+
return [...skills].sort((a, b) => a.id.localeCompare(b.id));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getSkillPaths(options: Pick<RuntimeProjectSkillCatalogOptions, "steeringPaths">) {
|
|
50
|
+
return options.steeringPaths?.skills ?? DEFAULT_PROJECT_STEERING_PATHS.skills;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getInstructionPaths(options: RuntimeProjectInstructionsOptions) {
|
|
54
|
+
return options.steeringPaths?.instructions ?? DEFAULT_PROJECT_STEERING_PATHS.instructions;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function loadRuntimeBuiltinSkillCatalog(input: {
|
|
58
|
+
skillsDir: string;
|
|
59
|
+
logger?: RuntimeSkillMetadataLogger;
|
|
60
|
+
}): RuntimeSkillDefinition[] {
|
|
61
|
+
const entriesResult = readRuntimeBuiltinSkillEntries(input.skillsDir);
|
|
62
|
+
if (!entriesResult.ok) {
|
|
63
|
+
input.logger?.error?.("Failed to load built-in skills", {
|
|
64
|
+
error: entriesResult.errorMessage,
|
|
65
|
+
skillsDir: input.skillsDir,
|
|
66
|
+
});
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return sortSkillsById(
|
|
71
|
+
entriesResult.entries.flatMap((entry) => {
|
|
72
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
73
|
+
const id = basename(entry.name, ".md");
|
|
74
|
+
const content = readRuntimeBuiltinFlatSkill(input.skillsDir, id);
|
|
75
|
+
if (content === null) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const definition = buildRuntimeSkillDefinition({
|
|
80
|
+
id,
|
|
81
|
+
content,
|
|
82
|
+
logger: input.logger,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return definition ? [definition] : [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
const content = readRuntimeBuiltinDirectorySkill(input.skillsDir, entry.name);
|
|
90
|
+
if (content === null) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const definition = buildRuntimeSkillDefinition({
|
|
95
|
+
id: entry.name,
|
|
96
|
+
content,
|
|
97
|
+
references: listRuntimeBuiltinSkillReferences(input.skillsDir, entry.name),
|
|
98
|
+
logger: input.logger,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return definition ? [definition] : [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [];
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getRuntimeProjectInstructions(
|
|
110
|
+
input: RuntimeProjectSteeringLookup & RuntimeProjectInstructionsOptions,
|
|
111
|
+
): Promise<string> {
|
|
112
|
+
for (const filePath of getInstructionPaths(input)) {
|
|
113
|
+
const file = await input.getProjectFile({
|
|
114
|
+
projectId: input.projectId,
|
|
115
|
+
authToken: input.authToken,
|
|
116
|
+
branchId: input.branchId,
|
|
117
|
+
path: filePath,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (file?.content) {
|
|
121
|
+
return file.content;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function getRuntimeProjectSkillCatalog(
|
|
129
|
+
input: RuntimeProjectSteeringLookup & RuntimeProjectSkillCatalogOptions,
|
|
130
|
+
): Promise<RuntimeSkillDefinition[]> {
|
|
131
|
+
const allFiles = await input.getProjectFiles({
|
|
132
|
+
projectId: input.projectId,
|
|
133
|
+
authToken: input.authToken,
|
|
134
|
+
branchId: input.branchId,
|
|
135
|
+
});
|
|
136
|
+
if (!allFiles || allFiles.length === 0) {
|
|
137
|
+
return [...input.builtinSkills];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const projectSkillsById = new Map<string, RuntimeSkillDefinition>();
|
|
141
|
+
|
|
142
|
+
for (const prefix of getSkillPaths(input)) {
|
|
143
|
+
const prefixWithSlash = `${prefix}/`;
|
|
144
|
+
|
|
145
|
+
const flatPaths = allFiles
|
|
146
|
+
.filter((file) => {
|
|
147
|
+
if (!file.path.startsWith(prefixWithSlash) || !file.path.endsWith(".md")) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const relative = file.path.slice(prefixWithSlash.length);
|
|
152
|
+
return !relative.includes("/");
|
|
153
|
+
})
|
|
154
|
+
.map((file) => file.path);
|
|
155
|
+
|
|
156
|
+
const dirPaths = allFiles
|
|
157
|
+
.filter((file) => file.path.startsWith(prefixWithSlash) && file.path.endsWith("/SKILL.md"))
|
|
158
|
+
.map((file) => file.path);
|
|
159
|
+
|
|
160
|
+
const skillPaths = [...dirPaths.sort(), ...flatPaths.sort()];
|
|
161
|
+
if (skillPaths.length === 0) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const skillFiles = await Promise.all(
|
|
166
|
+
skillPaths.map((path) =>
|
|
167
|
+
input.getProjectFile({
|
|
168
|
+
projectId: input.projectId,
|
|
169
|
+
authToken: input.authToken,
|
|
170
|
+
branchId: input.branchId,
|
|
171
|
+
path,
|
|
172
|
+
})
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
for (const file of skillFiles) {
|
|
177
|
+
if (!file?.content) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const isFlat = file.path.endsWith(".md") && !file.path.endsWith("/SKILL.md");
|
|
182
|
+
const id = getProjectSkillId(file.path, isFlat);
|
|
183
|
+
if (!id) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const definition = buildRuntimeSkillDefinition({
|
|
188
|
+
id,
|
|
189
|
+
content: file.content,
|
|
190
|
+
references: getProjectSkillReferences({ allFiles, file, isFlat }),
|
|
191
|
+
logger: input.logger,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (definition && !projectSkillsById.has(definition.id)) {
|
|
195
|
+
projectSkillsById.set(definition.id, definition);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (projectSkillsById.size === 0) {
|
|
201
|
+
return [...input.builtinSkills];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const mergedSkillsById = new Map(input.builtinSkills.map((skill) => [skill.id, skill]));
|
|
205
|
+
for (const skill of projectSkillsById.values()) {
|
|
206
|
+
mergedSkillsById.set(skill.id, skill);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return sortSkillsById(mergedSkillsById.values());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getProjectSkillId(path: string, isFlat: boolean): string | null {
|
|
213
|
+
const pathParts = path.split("/");
|
|
214
|
+
const fileName = pathParts.at(-1);
|
|
215
|
+
if (isFlat) {
|
|
216
|
+
return fileName ? basename(fileName, ".md") : null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return pathParts.at(-2) ?? null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getProjectSkillReferences(input: {
|
|
223
|
+
allFiles: readonly RuntimeProjectFileListItem[];
|
|
224
|
+
file: RuntimeProjectFile;
|
|
225
|
+
isFlat: boolean;
|
|
226
|
+
}): string[] {
|
|
227
|
+
if (input.isFlat) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const skillRootPrefix = input.file.path.replace(/SKILL\.md$/, "");
|
|
232
|
+
const refsPrefix = `${skillRootPrefix}references/`;
|
|
233
|
+
|
|
234
|
+
return input.allFiles
|
|
235
|
+
.filter((file) => file.path.startsWith(refsPrefix))
|
|
236
|
+
.map((file) => file.path.slice(skillRootPrefix.length))
|
|
237
|
+
.sort();
|
|
238
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PROJECT_STEERING_PATHS,
|
|
3
|
+
type ProjectSteeringPaths,
|
|
4
|
+
} from "./project-steering-mutation.js";
|
|
5
|
+
import type {
|
|
6
|
+
RuntimeGetProjectFileOptions,
|
|
7
|
+
RuntimeProjectFile,
|
|
8
|
+
RuntimeProjectFileListItem,
|
|
9
|
+
RuntimeProjectFilesApiOptions,
|
|
10
|
+
} from "./runtime-project-files-client.js";
|
|
11
|
+
import { normalizeRuntimeSkillReferencePath } from "./runtime-skill-metadata.js";
|
|
12
|
+
|
|
13
|
+
export type RuntimeProjectSkillContext = {
|
|
14
|
+
projectId?: string | null;
|
|
15
|
+
authToken: string;
|
|
16
|
+
branchId?: string | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type RuntimeLoadedProjectSkill = {
|
|
20
|
+
instructions: string;
|
|
21
|
+
references: string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type RuntimeProjectSkillLoaderLogger = {
|
|
25
|
+
warn?: (message: string, metadata?: Record<string, unknown>) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type RuntimeProjectSkillLoaderOptions = {
|
|
29
|
+
getProjectFile: (options: RuntimeGetProjectFileOptions) => Promise<RuntimeProjectFile | null>;
|
|
30
|
+
getProjectFiles: (
|
|
31
|
+
options: RuntimeProjectFilesApiOptions,
|
|
32
|
+
) => Promise<RuntimeProjectFileListItem[]>;
|
|
33
|
+
steeringPaths?: Pick<ProjectSteeringPaths, "skills">;
|
|
34
|
+
isAccessDeniedError?: (error: unknown) => boolean;
|
|
35
|
+
logger?: RuntimeProjectSkillLoaderLogger;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type RuntimeProjectSkillLoader = {
|
|
39
|
+
listProjectSkillReferences: (
|
|
40
|
+
context: RuntimeProjectSkillContext,
|
|
41
|
+
skillId: string,
|
|
42
|
+
) => Promise<string[]>;
|
|
43
|
+
loadProjectSkill: (
|
|
44
|
+
context: RuntimeProjectSkillContext,
|
|
45
|
+
skillId: string,
|
|
46
|
+
) => Promise<RuntimeLoadedProjectSkill | null>;
|
|
47
|
+
loadProjectSkillReference: (
|
|
48
|
+
context: RuntimeProjectSkillContext,
|
|
49
|
+
skillId: string,
|
|
50
|
+
normalizedFile: string,
|
|
51
|
+
) => Promise<string | null>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function getSkillPaths(options: RuntimeProjectSkillLoaderOptions): readonly string[] {
|
|
55
|
+
return options.steeringPaths?.skills ?? DEFAULT_PROJECT_STEERING_PATHS.skills;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isAccessDeniedError(
|
|
59
|
+
error: unknown,
|
|
60
|
+
options: RuntimeProjectSkillLoaderOptions,
|
|
61
|
+
): boolean {
|
|
62
|
+
return options.isAccessDeniedError?.(error) ?? false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function listProjectSkillReferences(input: {
|
|
66
|
+
options: RuntimeProjectSkillLoaderOptions;
|
|
67
|
+
context: RuntimeProjectSkillContext;
|
|
68
|
+
skillId: string;
|
|
69
|
+
}): Promise<string[]> {
|
|
70
|
+
const projectId = input.context.projectId;
|
|
71
|
+
if (!projectId) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const allFiles = await input.options.getProjectFiles({
|
|
76
|
+
projectId,
|
|
77
|
+
authToken: input.context.authToken,
|
|
78
|
+
branchId: input.context.branchId,
|
|
79
|
+
});
|
|
80
|
+
const references = new Set<string>();
|
|
81
|
+
|
|
82
|
+
for (const skillsPath of getSkillPaths(input.options)) {
|
|
83
|
+
const skillPrefix = `${skillsPath}/${input.skillId}/`;
|
|
84
|
+
const refsPrefix = `${skillPrefix}references/`;
|
|
85
|
+
|
|
86
|
+
for (const file of allFiles) {
|
|
87
|
+
if (!file.path.startsWith(refsPrefix)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const relativePath = file.path.slice(skillPrefix.length);
|
|
92
|
+
if (!relativePath.includes("/")) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const normalizedReference = normalizeRuntimeSkillReferencePath(relativePath);
|
|
97
|
+
if (normalizedReference) {
|
|
98
|
+
references.add(normalizedReference);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [...references].sort();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function loadProjectSkill(input: {
|
|
107
|
+
options: RuntimeProjectSkillLoaderOptions;
|
|
108
|
+
context: RuntimeProjectSkillContext;
|
|
109
|
+
skillId: string;
|
|
110
|
+
}): Promise<RuntimeLoadedProjectSkill | null> {
|
|
111
|
+
const projectId = input.context.projectId;
|
|
112
|
+
if (!projectId) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
for (const skillsPath of getSkillPaths(input.options)) {
|
|
118
|
+
const directorySkill = await input.options.getProjectFile({
|
|
119
|
+
projectId,
|
|
120
|
+
authToken: input.context.authToken,
|
|
121
|
+
branchId: input.context.branchId,
|
|
122
|
+
path: `${skillsPath}/${input.skillId}/SKILL.md`,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (directorySkill?.content) {
|
|
126
|
+
return {
|
|
127
|
+
instructions: directorySkill.content,
|
|
128
|
+
references: await listProjectSkillReferences(input),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const flatSkill = await input.options.getProjectFile({
|
|
133
|
+
projectId,
|
|
134
|
+
authToken: input.context.authToken,
|
|
135
|
+
branchId: input.context.branchId,
|
|
136
|
+
path: `${skillsPath}/${input.skillId}.md`,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (flatSkill?.content) {
|
|
140
|
+
return {
|
|
141
|
+
instructions: flatSkill.content,
|
|
142
|
+
references: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (isAccessDeniedError(error, input.options)) {
|
|
148
|
+
input.options.logger?.warn?.(
|
|
149
|
+
"Falling back to builtin skill after project skill lookup was denied",
|
|
150
|
+
{
|
|
151
|
+
projectId,
|
|
152
|
+
branchId: input.context.branchId ?? null,
|
|
153
|
+
skillId: input.skillId,
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function loadProjectSkillReference(input: {
|
|
166
|
+
options: RuntimeProjectSkillLoaderOptions;
|
|
167
|
+
context: RuntimeProjectSkillContext;
|
|
168
|
+
skillId: string;
|
|
169
|
+
normalizedFile: string;
|
|
170
|
+
}): Promise<string | null> {
|
|
171
|
+
const projectId = input.context.projectId;
|
|
172
|
+
if (!projectId) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
for (const skillsPath of getSkillPaths(input.options)) {
|
|
178
|
+
const projectFile = await input.options.getProjectFile({
|
|
179
|
+
projectId,
|
|
180
|
+
authToken: input.context.authToken,
|
|
181
|
+
branchId: input.context.branchId,
|
|
182
|
+
path: `${skillsPath}/${input.skillId}/${input.normalizedFile}`,
|
|
183
|
+
});
|
|
184
|
+
if (projectFile?.content) {
|
|
185
|
+
return projectFile.content;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (!isAccessDeniedError(error, input.options)) {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
input.options.logger?.warn?.(
|
|
194
|
+
"Falling back to builtin skill reference after project skill lookup was denied",
|
|
195
|
+
{
|
|
196
|
+
projectId,
|
|
197
|
+
branchId: input.context.branchId ?? null,
|
|
198
|
+
skillId: input.skillId,
|
|
199
|
+
file: input.normalizedFile,
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createRuntimeProjectSkillLoader(
|
|
208
|
+
options: RuntimeProjectSkillLoaderOptions,
|
|
209
|
+
): RuntimeProjectSkillLoader {
|
|
210
|
+
return {
|
|
211
|
+
listProjectSkillReferences: (context, skillId) =>
|
|
212
|
+
listProjectSkillReferences({ options, context, skillId }),
|
|
213
|
+
loadProjectSkill: (context, skillId) => loadProjectSkill({ options, context, skillId }),
|
|
214
|
+
loadProjectSkillReference: (context, skillId, normalizedFile) =>
|
|
215
|
+
loadProjectSkillReference({ options, context, skillId, normalizedFile }),
|
|
216
|
+
};
|
|
217
|
+
}
|