@stacksjs/ai 0.70.258 → 0.70.259
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/claude/index.js +20 -192
- package/dist/agents/index.js +1 -1
- package/dist/buddy.js +12 -304
- package/dist/client.js +3 -193
- package/dist/context.js +8 -199
- package/dist/drivers/anthropic/index.js +3 -276
- package/dist/drivers/claude-agent-sdk/index.js +3 -196
- package/dist/drivers/index.js +1 -4
- package/dist/drivers/ollama/index.js +4 -334
- package/dist/drivers/openai/index.js +3 -368
- package/dist/image.js +1 -375
- package/dist/index.js +1 -18
- package/dist/mcp.js +4 -361
- package/dist/personalization.js +13 -222
- package/dist/search.js +4 -310
- package/dist/text.js +1 -51
- package/dist/utils/client-bedrock-runtime.js +1 -17
- package/dist/utils/client-bedrock.js +1 -20
- package/dist/utils/model-access.js +1 -21
- package/dist/utils/retry.js +1 -39
- package/dist/utils/tokens.js +1 -59
- package/dist/utils/usage.js +1 -27
- package/dist/utils/vision.js +1 -54
- package/package.json +11 -4
package/dist/client.js
CHANGED
|
@@ -1,194 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import * as openai from "./drivers/openai";
|
|
4
|
-
function textContent(content) {
|
|
5
|
-
if (typeof content === "string")
|
|
6
|
-
return content;
|
|
7
|
-
return content.filter((block) => block.type === "text").map((block) => block.text ?? "").join(`
|
|
8
|
-
`);
|
|
9
|
-
}
|
|
10
|
-
export function resolveAIProvider(config, override) {
|
|
11
|
-
if (override)
|
|
12
|
-
return override;
|
|
13
|
-
const configured = String(config.default || "").toLowerCase();
|
|
14
|
-
if (configured === "anthropic" || configured.startsWith("anthropic."))
|
|
15
|
-
return "anthropic";
|
|
16
|
-
if (configured === "openai" || configured.startsWith("gpt-") || configured.startsWith("o1") || configured.startsWith("o3"))
|
|
17
|
-
return "openai";
|
|
18
|
-
if (configured === "ollama")
|
|
19
|
-
return "ollama";
|
|
20
|
-
throw Error(`Unsupported configured AI driver: ${config.default || "(empty)"}. Expected anthropic, openai, or ollama.`);
|
|
21
|
-
}
|
|
22
|
-
function configuredModel(config, provider) {
|
|
23
|
-
const driverModel = config.drivers?.[provider]?.model?.trim();
|
|
24
|
-
if (driverModel)
|
|
25
|
-
return driverModel;
|
|
26
|
-
const configuredDefault = String(config.default || "").trim();
|
|
27
|
-
if (!configuredDefault || configuredDefault.toLowerCase() === provider)
|
|
28
|
-
return;
|
|
29
|
-
return configuredDefault;
|
|
30
|
-
}
|
|
31
|
-
export function getAIProviderConfiguration(config, override, environment = process.env) {
|
|
32
|
-
const provider = resolveAIProvider(config, override), driver = config.drivers?.[provider] ?? {}, model = configuredModel(config, provider);
|
|
33
|
-
if (provider === "ollama")
|
|
34
|
-
return { provider, model, configured: !0, source: "local" };
|
|
35
|
-
if (driver.apiKey?.trim())
|
|
36
|
-
return { provider, model, configured: !0, source: "config" };
|
|
37
|
-
if (environment[provider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY"]?.trim())
|
|
38
|
-
return { provider, model, configured: !0, source: "environment" };
|
|
39
|
-
if (driver.baseUrl?.trim())
|
|
40
|
-
return { provider, model, configured: !0, source: "base-url" };
|
|
41
|
-
return { provider, model, configured: !1, source: "none" };
|
|
42
|
-
}
|
|
43
|
-
function parseObject(content) {
|
|
44
|
-
const unfenced = content.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
45
|
-
try {
|
|
46
|
-
return JSON.parse(unfenced);
|
|
47
|
-
} catch {
|
|
48
|
-
const start = unfenced.indexOf("{"), end = unfenced.lastIndexOf("}");
|
|
49
|
-
if (start >= 0 && end > start)
|
|
50
|
-
return JSON.parse(unfenced.slice(start, end + 1));
|
|
51
|
-
throw Error("AI response did not contain a valid JSON object");
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function typeMatches(value, type) {
|
|
55
|
-
if (type === "array")
|
|
56
|
-
return Array.isArray(value);
|
|
57
|
-
if (type === "integer")
|
|
58
|
-
return typeof value === "number" && Number.isInteger(value);
|
|
59
|
-
if (type === "object")
|
|
60
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
61
|
-
if (type === "null")
|
|
62
|
-
return value === null;
|
|
63
|
-
return typeof value === type;
|
|
64
|
-
}
|
|
65
|
-
function validateSchema(value, schema, path = "$") {
|
|
66
|
-
const errors = [];
|
|
67
|
-
if (schema.type) {
|
|
68
|
-
const allowed = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
69
|
-
if (!allowed.some((type) => typeMatches(value, type)))
|
|
70
|
-
return [`${path} must be ${allowed.join(" or ")}`];
|
|
71
|
-
}
|
|
72
|
-
if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value)))
|
|
73
|
-
errors.push(`${path} must be one of ${schema.enum.join(", ")}`);
|
|
74
|
-
if (typeof value === "string") {
|
|
75
|
-
if (schema.minLength !== void 0 && value.length < schema.minLength)
|
|
76
|
-
errors.push(`${path} must contain at least ${schema.minLength} characters`);
|
|
77
|
-
if (schema.maxLength !== void 0 && value.length > schema.maxLength)
|
|
78
|
-
errors.push(`${path} must contain at most ${schema.maxLength} characters`);
|
|
79
|
-
}
|
|
80
|
-
if (typeof value === "number") {
|
|
81
|
-
if (schema.minimum !== void 0 && value < schema.minimum)
|
|
82
|
-
errors.push(`${path} must be at least ${schema.minimum}`);
|
|
83
|
-
if (schema.maximum !== void 0 && value > schema.maximum)
|
|
84
|
-
errors.push(`${path} must be at most ${schema.maximum}`);
|
|
85
|
-
}
|
|
86
|
-
if (Array.isArray(value)) {
|
|
87
|
-
if (schema.minItems !== void 0 && value.length < schema.minItems)
|
|
88
|
-
errors.push(`${path} must contain at least ${schema.minItems} items`);
|
|
89
|
-
if (schema.maxItems !== void 0 && value.length > schema.maxItems)
|
|
90
|
-
errors.push(`${path} must contain at most ${schema.maxItems} items`);
|
|
91
|
-
if (schema.items)
|
|
92
|
-
value.forEach((item, index) => errors.push(...validateSchema(item, schema.items, `${path}[${index}]`)));
|
|
93
|
-
}
|
|
94
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
95
|
-
const record = value;
|
|
96
|
-
for (const key of schema.required ?? [])
|
|
97
|
-
if (!(key in record))
|
|
98
|
-
errors.push(`${path}.${key} is required`);
|
|
99
|
-
for (const [key, child] of Object.entries(schema.properties ?? {}))
|
|
100
|
-
if (key in record)
|
|
101
|
-
errors.push(...validateSchema(record[key], child, `${path}.${key}`));
|
|
102
|
-
if (schema.additionalProperties === !1) {
|
|
103
|
-
const known = new Set(Object.keys(schema.properties ?? {}));
|
|
104
|
-
for (const key of Object.keys(record))
|
|
105
|
-
if (!known.has(key))
|
|
106
|
-
errors.push(`${path}.${key} is not allowed`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return errors;
|
|
110
|
-
}
|
|
111
|
-
function createGenerate(config, provider) {
|
|
112
|
-
if (provider === "anthropic") {
|
|
113
|
-
const driver = config.drivers?.anthropic ?? {};
|
|
114
|
-
anthropic.configure({
|
|
115
|
-
apiKey: driver.apiKey || process.env.ANTHROPIC_API_KEY || "",
|
|
116
|
-
model: driver.model,
|
|
117
|
-
maxTokens: driver.maxTokens,
|
|
118
|
-
anthropicVersion: driver.anthropicVersion
|
|
119
|
-
});
|
|
120
|
-
return async (messages, options = {}) => {
|
|
121
|
-
const systemMessages = messages.filter((message) => message.role === "system").map((message) => textContent(message.content)), nonSystem = messages.filter((message) => message.role !== "system");
|
|
122
|
-
return anthropic.chat(nonSystem, {
|
|
123
|
-
...options,
|
|
124
|
-
system: [options.system, ...systemMessages].filter(Boolean).join(`
|
|
1
|
+
import*as anthropic from"./drivers/anthropic";import*as ollama from"./drivers/ollama";import*as openai from"./drivers/openai";function textContent(content){if(typeof content==="string")return content;return content.filter((block)=>block.type==="text").map((block)=>block.text??"").join(`
|
|
2
|
+
`)}export function resolveAIProvider(config,override){if(override)return override;const configured=String(config.default||"").toLowerCase();if(configured==="anthropic"||configured.startsWith("anthropic."))return"anthropic";if(configured==="openai"||configured.startsWith("gpt-")||configured.startsWith("o1")||configured.startsWith("o3"))return"openai";if(configured==="ollama")return"ollama";throw Error(`Unsupported configured AI driver: ${config.default||"(empty)"}. Expected anthropic, openai, or ollama.`)}function configuredModel(config,provider){const driverModel=config.drivers?.[provider]?.model?.trim();if(driverModel)return driverModel;const configuredDefault=String(config.default||"").trim();if(!configuredDefault||configuredDefault.toLowerCase()===provider)return;return configuredDefault}export function getAIProviderConfiguration(config,override,environment=process.env){const provider=resolveAIProvider(config,override),driver=config.drivers?.[provider]??{},model=configuredModel(config,provider);if(provider==="ollama")return{provider,model,configured:!0,source:"local"};if(driver.apiKey?.trim())return{provider,model,configured:!0,source:"config"};if(environment[provider==="openai"?"OPENAI_API_KEY":"ANTHROPIC_API_KEY"]?.trim())return{provider,model,configured:!0,source:"environment"};if(driver.baseUrl?.trim())return{provider,model,configured:!0,source:"base-url"};return{provider,model,configured:!1,source:"none"}}function parseObject(content){const unfenced=content.trim().replace(/^```(?:json)?\s*/i,"").replace(/\s*```$/,"");try{return JSON.parse(unfenced)}catch{const start=unfenced.indexOf("{"),end=unfenced.lastIndexOf("}");if(start>=0&&end>start)return JSON.parse(unfenced.slice(start,end+1));throw Error("AI response did not contain a valid JSON object")}}function typeMatches(value,type){if(type==="array")return Array.isArray(value);if(type==="integer")return typeof value==="number"&&Number.isInteger(value);if(type==="object")return typeof value==="object"&&value!==null&&!Array.isArray(value);if(type==="null")return value===null;return typeof value===type}function validateSchema(value,schema,path="$"){const errors=[];if(schema.type){const allowed=Array.isArray(schema.type)?schema.type:[schema.type];if(!allowed.some((type)=>typeMatches(value,type)))return[`${path} must be ${allowed.join(" or ")}`]}if(schema.enum&&!schema.enum.some((candidate)=>Object.is(candidate,value)))errors.push(`${path} must be one of ${schema.enum.join(", ")}`);if(typeof value==="string"){if(schema.minLength!==void 0&&value.length<schema.minLength)errors.push(`${path} must contain at least ${schema.minLength} characters`);if(schema.maxLength!==void 0&&value.length>schema.maxLength)errors.push(`${path} must contain at most ${schema.maxLength} characters`)}if(typeof value==="number"){if(schema.minimum!==void 0&&value<schema.minimum)errors.push(`${path} must be at least ${schema.minimum}`);if(schema.maximum!==void 0&&value>schema.maximum)errors.push(`${path} must be at most ${schema.maximum}`)}if(Array.isArray(value)){if(schema.minItems!==void 0&&value.length<schema.minItems)errors.push(`${path} must contain at least ${schema.minItems} items`);if(schema.maxItems!==void 0&&value.length>schema.maxItems)errors.push(`${path} must contain at most ${schema.maxItems} items`);if(schema.items)value.forEach((item,index)=>errors.push(...validateSchema(item,schema.items,`${path}[${index}]`)))}if(typeof value==="object"&&value!==null&&!Array.isArray(value)){const record=value;for(const key of schema.required??[])if(!(key in record))errors.push(`${path}.${key} is required`);for(const[key,child]of Object.entries(schema.properties??{}))if(key in record)errors.push(...validateSchema(record[key],child,`${path}.${key}`));if(schema.additionalProperties===!1){const known=new Set(Object.keys(schema.properties??{}));for(const key of Object.keys(record))if(!known.has(key))errors.push(`${path}.${key} is not allowed`)}}return errors}function createGenerate(config,provider){if(provider==="anthropic"){const driver=config.drivers?.anthropic??{};anthropic.configure({apiKey:driver.apiKey||process.env.ANTHROPIC_API_KEY||"",model:driver.model,maxTokens:driver.maxTokens,anthropicVersion:driver.anthropicVersion});return async(messages,options={})=>{const systemMessages=messages.filter((message)=>message.role==="system").map((message)=>textContent(message.content)),nonSystem=messages.filter((message)=>message.role!=="system");return anthropic.chat(nonSystem,{...options,system:[options.system,...systemMessages].filter(Boolean).join(`
|
|
125
3
|
|
|
126
|
-
`) ||
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
if (provider === "openai") {
|
|
131
|
-
const driver = config.drivers?.openai ?? {};
|
|
132
|
-
openai.configure({
|
|
133
|
-
apiKey: driver.apiKey || process.env.OPENAI_API_KEY || "",
|
|
134
|
-
model: driver.model,
|
|
135
|
-
maxTokens: driver.maxTokens,
|
|
136
|
-
baseUrl: driver.baseUrl,
|
|
137
|
-
embeddingModel: driver.embeddingModel
|
|
138
|
-
});
|
|
139
|
-
return (messages, options = {}) => {
|
|
140
|
-
const finalMessages = options.system ? [{ role: "system", content: options.system }, ...messages] : messages;
|
|
141
|
-
return openai.chat(finalMessages, options);
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
const driver = config.drivers?.ollama ?? {};
|
|
145
|
-
ollama.configure({
|
|
146
|
-
host: driver.host || driver.baseUrl,
|
|
147
|
-
model: driver.model,
|
|
148
|
-
embeddingModel: driver.embeddingModel
|
|
149
|
-
});
|
|
150
|
-
return (messages, options = {}) => {
|
|
151
|
-
const finalMessages = options.system ? [{ role: "system", content: options.system }, ...messages] : messages;
|
|
152
|
-
return ollama.chat(finalMessages, options);
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
export function createAIClient(config, override) {
|
|
156
|
-
const provider = resolveAIProvider(config, override), configuration = getAIProviderConfiguration(config, provider), generate = createGenerate(config, provider);
|
|
157
|
-
return {
|
|
158
|
-
provider,
|
|
159
|
-
configuration,
|
|
160
|
-
generate,
|
|
161
|
-
async generateObject(messages, schema, options = {}) {
|
|
162
|
-
const { attempts = 2, system, ...completionOptions } = options, history = [...messages];
|
|
163
|
-
let lastError;
|
|
164
|
-
for (let attempt = 1;attempt <= Math.max(1, attempts); attempt++) {
|
|
165
|
-
const result = await generate(history, {
|
|
166
|
-
...completionOptions,
|
|
167
|
-
system,
|
|
168
|
-
responseFormat: {
|
|
169
|
-
type: "json_schema",
|
|
170
|
-
json_schema: { name: "structured_output", schema, strict: !0 }
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
try {
|
|
174
|
-
const data = parseObject(result.content), errors = validateSchema(data, schema);
|
|
175
|
-
if (errors.length)
|
|
176
|
-
throw Error(`AI response failed schema validation: ${errors.slice(0, 8).join("; ")}`);
|
|
177
|
-
return { data, result };
|
|
178
|
-
} catch (error) {
|
|
179
|
-
lastError = error instanceof Error ? error : Error(String(error));
|
|
180
|
-
if (attempt < attempts) {
|
|
181
|
-
history.push({ role: "assistant", content: result.content });
|
|
182
|
-
history.push({
|
|
183
|
-
role: "user",
|
|
184
|
-
content: `Return only a corrected JSON object. Validation error: ${lastError.message}`
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
throw lastError ?? Error("AI object generation failed");
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export { parseObject as parseAIObject, validateSchema as validateAISchema };
|
|
4
|
+
`)||void 0})}}if(provider==="openai"){const driver=config.drivers?.openai??{};openai.configure({apiKey:driver.apiKey||process.env.OPENAI_API_KEY||"",model:driver.model,maxTokens:driver.maxTokens,baseUrl:driver.baseUrl,embeddingModel:driver.embeddingModel});return(messages,options={})=>{const finalMessages=options.system?[{role:"system",content:options.system},...messages]:messages;return openai.chat(finalMessages,options)}}const driver=config.drivers?.ollama??{};ollama.configure({host:driver.host||driver.baseUrl,model:driver.model,embeddingModel:driver.embeddingModel});return(messages,options={})=>{const finalMessages=options.system?[{role:"system",content:options.system},...messages]:messages;return ollama.chat(finalMessages,options)}}export function createAIClient(config,override){const provider=resolveAIProvider(config,override),configuration=getAIProviderConfiguration(config,provider),generate=createGenerate(config,provider);return{provider,configuration,generate,async generateObject(messages,schema,options={}){const{attempts=2,system,...completionOptions}=options,history=[...messages];let lastError;for(let attempt=1;attempt<=Math.max(1,attempts);attempt++){const result=await generate(history,{...completionOptions,system,responseFormat:{type:"json_schema",json_schema:{name:"structured_output",schema,strict:!0}}});try{const data=parseObject(result.content),errors=validateSchema(data,schema);if(errors.length)throw Error(`AI response failed schema validation: ${errors.slice(0,8).join("; ")}`);return{data,result}}catch(error){lastError=error instanceof Error?error:Error(String(error));if(attempt<attempts){history.push({role:"assistant",content:result.content});history.push({role:"user",content:`Return only a corrected JSON object. Validation error: ${lastError.message}`})}}}throw lastError??Error("AI object generation failed")}}}export{parseObject as parseAIObject,validateSchema as validateAISchema};
|
package/dist/context.js
CHANGED
|
@@ -1,200 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
".cache",
|
|
9
|
-
".next",
|
|
10
|
-
".output",
|
|
11
|
-
".turbo",
|
|
12
|
-
".vite",
|
|
13
|
-
"build",
|
|
14
|
-
"coverage",
|
|
15
|
-
"dist",
|
|
16
|
-
"node_modules",
|
|
17
|
-
"tmp",
|
|
18
|
-
"vendor"
|
|
19
|
-
]), sensitiveNames = /^(?:\.env(?:\..*)?|\.npmrc|\.pypirc|credentials(?:\..*)?|id_(?:rsa|ed25519)(?:\.pub)?|secrets?(?:\..*)?)$/i, lockNames = /(?:^|\.)(?:bun|package-lock|pnpm-lock|yarn|pantry)\.?(?:lock|yaml)?$/i, surfaceDefinitions = [
|
|
20
|
-
{ id: "models", purpose: "Domain schema, validation, relationships, factories, and traits.", prefixes: ["app/Models/", "storage/framework/defaults/app/Models/"] },
|
|
21
|
-
{ id: "actions", purpose: "Transport-independent application behavior.", prefixes: ["app/Actions/", "storage/framework/defaults/app/Actions/"] },
|
|
22
|
-
{ id: "routes", purpose: "HTTP and API route registration.", prefixes: ["routes/", "app/Routes.ts"] },
|
|
23
|
-
{ id: "jobs", purpose: "Background and inline work.", prefixes: ["app/Jobs/", "storage/framework/defaults/app/Jobs/"] },
|
|
24
|
-
{ id: "configuration", purpose: "Typed application and provider configuration.", prefixes: ["config/"] },
|
|
25
|
-
{ id: "views", purpose: "STX views, layouts, components, and client functions.", prefixes: ["resources/"] },
|
|
26
|
-
{ id: "database", purpose: "Reviewable migrations and seed data.", prefixes: ["database/"] },
|
|
27
|
-
{ id: "tests", purpose: "Unit, feature, contract, and end-to-end evidence.", prefixes: ["tests/", "storage/framework/core/"] }
|
|
28
|
-
];
|
|
29
|
-
function normalizePath(path) {
|
|
30
|
-
return path.split(sep).join("/");
|
|
31
|
-
}
|
|
32
|
-
function isExcluded(path) {
|
|
33
|
-
const segments = path.split("/"), name = basename(path);
|
|
34
|
-
return segments.some((segment) => excludedSegments.has(segment)) || sensitiveNames.test(name) || lockNames.test(name) || name.endsWith(".lock");
|
|
35
|
-
}
|
|
36
|
-
function collectFiles(root) {
|
|
37
|
-
const files = [];
|
|
38
|
-
function visit(directory) {
|
|
39
|
-
const entries = readdirSync(directory, { withFileTypes: !0 }).sort((a, b) => a.name.localeCompare(b.name));
|
|
40
|
-
for (const entry of entries) {
|
|
41
|
-
const fullPath = resolve(directory, entry.name), projectPath = normalizePath(relative(root, fullPath));
|
|
42
|
-
if (isExcluded(projectPath))
|
|
43
|
-
continue;
|
|
44
|
-
if (entry.isDirectory())
|
|
45
|
-
visit(fullPath);
|
|
46
|
-
else if (entry.isFile() && !lstatSync(fullPath).isSymbolicLink())
|
|
47
|
-
files.push(projectPath);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
visit(root);
|
|
51
|
-
return files.sort((a, b) => a.localeCompare(b));
|
|
52
|
-
}
|
|
53
|
-
function readPackageSummary(root) {
|
|
54
|
-
const packagePath = resolve(root, "package.json");
|
|
55
|
-
if (!existsSync(packagePath))
|
|
56
|
-
return { name: null, version: null, scripts: [], dependencies: [] };
|
|
57
|
-
try {
|
|
58
|
-
const manifest = JSON.parse(readFileSync(packagePath, "utf8")), dependencies = [
|
|
59
|
-
...Object.keys(manifest.dependencies || {}),
|
|
60
|
-
...Object.keys(manifest.devDependencies || {}),
|
|
61
|
-
...Object.keys(manifest.peerDependencies || {})
|
|
62
|
-
];
|
|
63
|
-
return {
|
|
64
|
-
name: typeof manifest.name === "string" ? manifest.name : null,
|
|
65
|
-
version: typeof manifest.version === "string" ? manifest.version : null,
|
|
66
|
-
scripts: Object.keys(manifest.scripts || {}).sort().slice(0, 25),
|
|
67
|
-
dependencies: [...new Set(dependencies)].sort().slice(0, 30)
|
|
68
|
-
};
|
|
69
|
-
} catch {
|
|
70
|
-
return { name: null, version: null, scripts: [], dependencies: [] };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
function filePriority(path) {
|
|
74
|
-
if (/^(?:AGENTS|CLAUDE)\.md$|^\.github\/copilot-instructions\.md$/.test(path))
|
|
75
|
-
return 0;
|
|
76
|
-
if (path === "package.json" || path === "app/Routes.ts" || path === "app/Commands.ts")
|
|
77
|
-
return 1;
|
|
78
|
-
if (/^(?:app|routes|config)\//.test(path))
|
|
79
|
-
return 2;
|
|
80
|
-
if (/^(?:resources|database|tests)\//.test(path))
|
|
81
|
-
return 3;
|
|
82
|
-
if (/^docs\//.test(path))
|
|
83
|
-
return 4;
|
|
84
|
-
if (/^storage\/framework\/defaults\//.test(path))
|
|
85
|
-
return 5;
|
|
86
|
-
return 6;
|
|
87
|
-
}
|
|
88
|
-
function representativeFiles(files, maxFiles) {
|
|
89
|
-
return [...files].sort((a, b) => filePriority(a) - filePriority(b) || a.localeCompare(b)).slice(0, maxFiles).sort((a, b) => a.localeCompare(b));
|
|
90
|
-
}
|
|
91
|
-
function surfacePaths(files, prefixes) {
|
|
92
|
-
return files.filter((file) => prefixes.some((prefix) => prefix.endsWith("/") ? file.startsWith(prefix) : file === prefix)).slice(0, 4);
|
|
93
|
-
}
|
|
94
|
-
function renderContext(context) {
|
|
95
|
-
return [
|
|
96
|
-
"Stacks AI Project Context v1",
|
|
97
|
-
"",
|
|
98
|
-
`Project: ${context.project.name || "unnamed"}${context.project.version ? `@${context.project.version}` : ""}`,
|
|
99
|
-
"Architecture: Model-View-Action",
|
|
100
|
-
`Override rule: ${context.architecture.overrideRule}`,
|
|
101
|
-
`Authoring order: ${context.architecture.authoringOrder.join(" -> ")}`,
|
|
102
|
-
"",
|
|
103
|
-
"Authoring principles:",
|
|
104
|
-
...context.architecture.principles.map((principle) => `- ${principle}`),
|
|
105
|
-
"",
|
|
106
|
-
"Instruction files:",
|
|
107
|
-
...context.instructionFiles.length ? context.instructionFiles.map((file) => `- ${file}`) : ["- none detected"],
|
|
108
|
-
"",
|
|
109
|
-
"Available surfaces:",
|
|
110
|
-
...context.surfaces.flatMap((surface) => [
|
|
111
|
-
`- ${surface.id}: ${surface.purpose}`,
|
|
112
|
-
...surface.paths.map((path) => ` - ${path}`)
|
|
113
|
-
]),
|
|
114
|
-
"",
|
|
115
|
-
`Scripts: ${context.project.scripts.join(", ") || "none detected"}`,
|
|
116
|
-
`Dependencies: ${context.project.dependencies.join(", ") || "none detected"}`,
|
|
117
|
-
"",
|
|
118
|
-
"Representative files:",
|
|
119
|
-
...context.representativeFiles.map((file) => `- ${file}`)
|
|
120
|
-
].join(`
|
|
121
|
-
`);
|
|
122
|
-
}
|
|
123
|
-
function fitToBudget(text, maxChars) {
|
|
124
|
-
if (text.length <= maxChars)
|
|
125
|
-
return { text, truncated: !1 };
|
|
126
|
-
const marker = `
|
|
127
|
-
[context truncated by character budget]`;
|
|
128
|
-
if (maxChars <= marker.length)
|
|
129
|
-
return { text: marker.slice(0, maxChars), truncated: !0 };
|
|
130
|
-
const available = maxChars - marker.length, prefix = text.slice(0, available), lastLine = prefix.lastIndexOf(`
|
|
131
|
-
`);
|
|
132
|
-
return {
|
|
133
|
-
text: `${prefix.slice(0, lastLine > 0 ? lastLine : available)}${marker}`,
|
|
134
|
-
truncated: !0
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
function unstructuredBaseline(root, files) {
|
|
138
|
-
const readmePath = resolve(root, "README.md"), packagePath = resolve(root, "package.json"), readme = existsSync(readmePath) ? readFileSync(readmePath, "utf8").slice(0, 2000) : "", packageJson = existsSync(packagePath) ? readFileSync(packagePath, "utf8") : "";
|
|
139
|
-
return [
|
|
140
|
-
"Repository Structure:",
|
|
141
|
-
files.slice(0, 50).join(`
|
|
142
|
-
`),
|
|
143
|
-
readme ? `README.md (excerpt):
|
|
144
|
-
${readme}` : "",
|
|
145
|
-
packageJson ? `package.json:
|
|
146
|
-
${packageJson}` : ""
|
|
147
|
-
].filter(Boolean).join(`
|
|
1
|
+
import{existsSync,lstatSync,readFileSync,readdirSync}from"node:fs";import{basename,relative,resolve,sep}from"node:path";import{estimateTokens}from"./utils/tokens";export const STACKS_PROJECT_CONTEXT_SCHEMA="https://stacksjs.org/schemas/ai-project-context/v1";const DEFAULT_MAX_CHARS=4000,DEFAULT_MAX_FILES=30,excludedSegments=new Set([".git",".bun",".cache",".next",".output",".turbo",".vite","build","coverage","dist","node_modules","tmp","vendor"]),sensitiveNames=/^(?:\.env(?:\..*)?|\.npmrc|\.pypirc|credentials(?:\..*)?|id_(?:rsa|ed25519)(?:\.pub)?|secrets?(?:\..*)?)$/i,lockNames=/(?:^|\.)(?:bun|package-lock|pnpm-lock|yarn|pantry)\.?(?:lock|yaml)?$/i,surfaceDefinitions=[{id:"models",purpose:"Domain schema, validation, relationships, factories, and traits.",prefixes:["app/Models/","storage/framework/defaults/app/Models/"]},{id:"actions",purpose:"Transport-independent application behavior.",prefixes:["app/Actions/","storage/framework/defaults/app/Actions/"]},{id:"routes",purpose:"HTTP and API route registration.",prefixes:["routes/","app/Routes.ts"]},{id:"jobs",purpose:"Background and inline work.",prefixes:["app/Jobs/","storage/framework/defaults/app/Jobs/"]},{id:"configuration",purpose:"Typed application and provider configuration.",prefixes:["config/"]},{id:"views",purpose:"STX views, layouts, components, and client functions.",prefixes:["resources/"]},{id:"database",purpose:"Reviewable migrations and seed data.",prefixes:["database/"]},{id:"tests",purpose:"Unit, feature, contract, and end-to-end evidence.",prefixes:["tests/","storage/framework/core/"]}];function normalizePath(path){return path.split(sep).join("/")}function isExcluded(path){const segments=path.split("/"),name=basename(path);return segments.some((segment)=>excludedSegments.has(segment))||sensitiveNames.test(name)||lockNames.test(name)||name.endsWith(".lock")}function collectFiles(root){const files=[];function visit(directory){const entries=readdirSync(directory,{withFileTypes:!0}).sort((a,b)=>a.name.localeCompare(b.name));for(const entry of entries){const fullPath=resolve(directory,entry.name),projectPath=normalizePath(relative(root,fullPath));if(isExcluded(projectPath))continue;if(entry.isDirectory())visit(fullPath);else if(entry.isFile()&&!lstatSync(fullPath).isSymbolicLink())files.push(projectPath)}}visit(root);return files.sort((a,b)=>a.localeCompare(b))}function readPackageSummary(root){const packagePath=resolve(root,"package.json");if(!existsSync(packagePath))return{name:null,version:null,scripts:[],dependencies:[]};try{const manifest=JSON.parse(readFileSync(packagePath,"utf8")),dependencies=[...Object.keys(manifest.dependencies||{}),...Object.keys(manifest.devDependencies||{}),...Object.keys(manifest.peerDependencies||{})];return{name:typeof manifest.name==="string"?manifest.name:null,version:typeof manifest.version==="string"?manifest.version:null,scripts:Object.keys(manifest.scripts||{}).sort().slice(0,25),dependencies:[...new Set(dependencies)].sort().slice(0,30)}}catch{return{name:null,version:null,scripts:[],dependencies:[]}}}function filePriority(path){if(/^(?:AGENTS|CLAUDE)\.md$|^\.github\/copilot-instructions\.md$/.test(path))return 0;if(path==="package.json"||path==="app/Routes.ts"||path==="app/Commands.ts")return 1;if(/^(?:app|routes|config)\//.test(path))return 2;if(/^(?:resources|database|tests)\//.test(path))return 3;if(/^docs\//.test(path))return 4;if(/^storage\/framework\/defaults\//.test(path))return 5;return 6}function representativeFiles(files,maxFiles){return[...files].sort((a,b)=>filePriority(a)-filePriority(b)||a.localeCompare(b)).slice(0,maxFiles).sort((a,b)=>a.localeCompare(b))}function surfacePaths(files,prefixes){return files.filter((file)=>prefixes.some((prefix)=>prefix.endsWith("/")?file.startsWith(prefix):file===prefix)).slice(0,4)}function renderContext(context){return["Stacks AI Project Context v1","",`Project: ${context.project.name||"unnamed"}${context.project.version?`@${context.project.version}`:""}`,"Architecture: Model-View-Action",`Override rule: ${context.architecture.overrideRule}`,`Authoring order: ${context.architecture.authoringOrder.join(" -> ")}`,"","Authoring principles:",...context.architecture.principles.map((principle)=>`- ${principle}`),"","Instruction files:",...context.instructionFiles.length?context.instructionFiles.map((file)=>`- ${file}`):["- none detected"],"","Available surfaces:",...context.surfaces.flatMap((surface)=>[`- ${surface.id}: ${surface.purpose}`,...surface.paths.map((path)=>` - ${path}`)]),"",`Scripts: ${context.project.scripts.join(", ")||"none detected"}`,`Dependencies: ${context.project.dependencies.join(", ")||"none detected"}`,"","Representative files:",...context.representativeFiles.map((file)=>`- ${file}`)].join(`
|
|
2
|
+
`)}function fitToBudget(text,maxChars){if(text.length<=maxChars)return{text,truncated:!1};const marker=`
|
|
3
|
+
[context truncated by character budget]`;if(maxChars<=marker.length)return{text:marker.slice(0,maxChars),truncated:!0};const available=maxChars-marker.length,prefix=text.slice(0,available),lastLine=prefix.lastIndexOf(`
|
|
4
|
+
`);return{text:`${prefix.slice(0,lastLine>0?lastLine:available)}${marker}`,truncated:!0}}function unstructuredBaseline(root,files){const readmePath=resolve(root,"README.md"),packagePath=resolve(root,"package.json"),readme=existsSync(readmePath)?readFileSync(readmePath,"utf8").slice(0,2000):"",packageJson=existsSync(packagePath)?readFileSync(packagePath,"utf8"):"";return["Repository Structure:",files.slice(0,50).join(`
|
|
5
|
+
`),readme?`README.md (excerpt):
|
|
6
|
+
${readme}`:"",packageJson?`package.json:
|
|
7
|
+
${packageJson}`:""].filter(Boolean).join(`
|
|
148
8
|
|
|
149
|
-
`);
|
|
150
|
-
}
|
|
151
|
-
export function buildProjectContext(repoPath, options = {}) {
|
|
152
|
-
const root = resolve(repoPath), maxChars = options.maxChars ?? DEFAULT_MAX_CHARS, maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES, model = options.model || "gpt-4o";
|
|
153
|
-
if (!Number.isInteger(maxChars) || maxChars < 256)
|
|
154
|
-
throw Error("maxChars must be an integer of at least 256");
|
|
155
|
-
if (!Number.isInteger(maxFiles) || maxFiles < 1)
|
|
156
|
-
throw Error("maxFiles must be a positive integer");
|
|
157
|
-
const files = collectFiles(root), included = representativeFiles(files, maxFiles), context = {
|
|
158
|
-
schema: STACKS_PROJECT_CONTEXT_SCHEMA,
|
|
159
|
-
schemaVersion: "1.0.0",
|
|
160
|
-
framework: "stacks",
|
|
161
|
-
architecture: {
|
|
162
|
-
pattern: "Model-View-Action",
|
|
163
|
-
overrideRule: "Application files under app/ override matching framework defaults; do not edit defaults for application customization.",
|
|
164
|
-
authoringOrder: ["model", "migration", "action", "route", "test"],
|
|
165
|
-
principles: [
|
|
166
|
-
"Prefer conventional paths and framework generators over bespoke glue code.",
|
|
167
|
-
"Declare domain shape once and derive validation, migrations, API surfaces, and types where supported.",
|
|
168
|
-
"Keep Actions transport-independent and make unsupported drivers fail loudly.",
|
|
169
|
-
"Read repository instruction files before changing source."
|
|
170
|
-
]
|
|
171
|
-
},
|
|
172
|
-
project: readPackageSummary(root),
|
|
173
|
-
instructionFiles: files.filter((file) => /(?:^|\/)(?:AGENTS|CLAUDE)\.md$|^\.github\/copilot-instructions\.md$/.test(file)),
|
|
174
|
-
surfaces: surfaceDefinitions.map((surface) => ({ id: surface.id, purpose: surface.purpose, paths: surfacePaths(files, surface.prefixes) })).filter((surface) => surface.paths.length > 0),
|
|
175
|
-
representativeFiles: included,
|
|
176
|
-
exclusions: [
|
|
177
|
-
"dependency, build, cache, coverage, temporary, and vendor directories",
|
|
178
|
-
"lockfiles",
|
|
179
|
-
"environment, credential, private-key, and secret files"
|
|
180
|
-
]
|
|
181
|
-
}, fitted = fitToBudget(renderContext(context), maxChars), baseline = unstructuredBaseline(root, files), outputTokens = estimateTokens(fitted.text, model), baselineTokens = estimateTokens(baseline, model), reduction = baselineTokens > 0 ? Math.round((1 - outputTokens / baselineTokens) * 1000) / 10 : null;
|
|
182
|
-
return {
|
|
183
|
-
context,
|
|
184
|
-
text: fitted.text,
|
|
185
|
-
metrics: {
|
|
186
|
-
candidateFiles: files.length,
|
|
187
|
-
includedFiles: included.length,
|
|
188
|
-
maxCharacters: maxChars,
|
|
189
|
-
outputCharacters: fitted.text.length,
|
|
190
|
-
estimatedTokens: outputTokens,
|
|
191
|
-
model,
|
|
192
|
-
truncated: fitted.truncated,
|
|
193
|
-
baselineMethod: "sorted-first-50-paths+readme-2000+package-json",
|
|
194
|
-
baselineCharacters: baseline.length,
|
|
195
|
-
baselineEstimatedTokens: baselineTokens,
|
|
196
|
-
estimatedTokenReductionPercent: reduction,
|
|
197
|
-
tokenEstimateIsHeuristic: !0
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
}
|
|
9
|
+
`)}export function buildProjectContext(repoPath,options={}){const root=resolve(repoPath),maxChars=options.maxChars??DEFAULT_MAX_CHARS,maxFiles=options.maxFiles??DEFAULT_MAX_FILES,model=options.model||"gpt-4o";if(!Number.isInteger(maxChars)||maxChars<256)throw Error("maxChars must be an integer of at least 256");if(!Number.isInteger(maxFiles)||maxFiles<1)throw Error("maxFiles must be a positive integer");const files=collectFiles(root),included=representativeFiles(files,maxFiles),context={schema:STACKS_PROJECT_CONTEXT_SCHEMA,schemaVersion:"1.0.0",framework:"stacks",architecture:{pattern:"Model-View-Action",overrideRule:"Application files under app/ override matching framework defaults; do not edit defaults for application customization.",authoringOrder:["model","migration","action","route","test"],principles:["Prefer conventional paths and framework generators over bespoke glue code.","Declare domain shape once and derive validation, migrations, API surfaces, and types where supported.","Keep Actions transport-independent and make unsupported drivers fail loudly.","Read repository instruction files before changing source."]},project:readPackageSummary(root),instructionFiles:files.filter((file)=>/(?:^|\/)(?:AGENTS|CLAUDE)\.md$|^\.github\/copilot-instructions\.md$/.test(file)),surfaces:surfaceDefinitions.map((surface)=>({id:surface.id,purpose:surface.purpose,paths:surfacePaths(files,surface.prefixes)})).filter((surface)=>surface.paths.length>0),representativeFiles:included,exclusions:["dependency, build, cache, coverage, temporary, and vendor directories","lockfiles","environment, credential, private-key, and secret files"]},fitted=fitToBudget(renderContext(context),maxChars),baseline=unstructuredBaseline(root,files),outputTokens=estimateTokens(fitted.text,model),baselineTokens=estimateTokens(baseline,model),reduction=baselineTokens>0?Math.round((1-outputTokens/baselineTokens)*1000)/10:null;return{context,text:fitted.text,metrics:{candidateFiles:files.length,includedFiles:included.length,maxCharacters:maxChars,outputCharacters:fitted.text.length,estimatedTokens:outputTokens,model,truncated:fitted.truncated,baselineMethod:"sorted-first-50-paths+readme-2000+package-json",baselineCharacters:baseline.length,baselineEstimatedTokens:baselineTokens,estimatedTokenReductionPercent:reduction,tokenEstimateIsHeuristic:!0}}}
|