@t3lnet/sceneforge 1.0.9 → 1.0.10

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.
Files changed (32) hide show
  1. package/README.md +57 -0
  2. package/cli/cli.js +6 -0
  3. package/cli/commands/context.js +791 -0
  4. package/context/context-builder.ts +318 -0
  5. package/context/index.ts +52 -0
  6. package/context/template-loader.ts +161 -0
  7. package/context/templates/base/actions-reference.md +299 -0
  8. package/context/templates/base/cli-reference.md +236 -0
  9. package/context/templates/base/project-overview.md +58 -0
  10. package/context/templates/base/selectors-guide.md +233 -0
  11. package/context/templates/base/yaml-schema.md +210 -0
  12. package/context/templates/skills/balance-timing.md +136 -0
  13. package/context/templates/skills/debug-selector.md +193 -0
  14. package/context/templates/skills/generate-actions.md +94 -0
  15. package/context/templates/skills/optimize-demo.md +218 -0
  16. package/context/templates/skills/review-demo-yaml.md +164 -0
  17. package/context/templates/skills/write-step-script.md +136 -0
  18. package/context/templates/stages/stage1-actions.md +236 -0
  19. package/context/templates/stages/stage2-scripts.md +197 -0
  20. package/context/templates/stages/stage3-balancing.md +229 -0
  21. package/context/templates/stages/stage4-rebalancing.md +228 -0
  22. package/context/tests/context-builder.test.ts +237 -0
  23. package/context/tests/template-loader.test.ts +181 -0
  24. package/context/tests/tool-formatter.test.ts +198 -0
  25. package/context/tool-formatter.ts +189 -0
  26. package/dist/index.cjs +416 -11
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +182 -1
  29. package/dist/index.d.ts +182 -1
  30. package/dist/index.js +391 -11
  31. package/dist/index.js.map +1 -1
  32. package/package.json +2 -1
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Main context builder for generating LLM instruction files.
3
+ * Orchestrates template loading, composition, and formatting.
4
+ */
5
+
6
+ import * as fs from "fs/promises";
7
+ import * as path from "path";
8
+ import {
9
+ loadTemplate,
10
+ loadTemplatesByCategory,
11
+ interpolateVariables,
12
+ composeTemplates,
13
+ listTemplates,
14
+ type LoadedTemplate,
15
+ type TemplateVariables,
16
+ } from "./template-loader.js";
17
+ import {
18
+ formatForTool,
19
+ getOutputPath,
20
+ getToolConfig,
21
+ getSupportedTools,
22
+ formatStageName,
23
+ getStageFileName,
24
+ type TargetTool,
25
+ type DeployFormat,
26
+ } from "./tool-formatter.js";
27
+
28
+ export type Stage = "actions" | "scripts" | "balance" | "rebalance" | "all";
29
+
30
+ export interface ContextBuilderOptions {
31
+ target: TargetTool | "all";
32
+ stage: Stage;
33
+ format: DeployFormat;
34
+ outputDir: string;
35
+ variables?: TemplateVariables;
36
+ }
37
+
38
+ export interface DeployResult {
39
+ tool: TargetTool;
40
+ filePath: string;
41
+ stage?: string;
42
+ created: boolean;
43
+ skipped?: boolean;
44
+ error?: string;
45
+ }
46
+
47
+ export interface PreviewResult {
48
+ tool: TargetTool;
49
+ stage?: string;
50
+ content: string;
51
+ }
52
+
53
+ /**
54
+ * Build context content for a specific tool and stage.
55
+ */
56
+ export async function buildContext(
57
+ tool: TargetTool,
58
+ stage: Stage,
59
+ variables?: TemplateVariables
60
+ ): Promise<string> {
61
+ const templates: LoadedTemplate[] = [];
62
+
63
+ // Always load base templates
64
+ const baseTemplates = await loadTemplatesByCategory("base");
65
+ templates.push(...baseTemplates);
66
+
67
+ // Load stage-specific templates
68
+ if (stage === "all") {
69
+ const stageTemplates = await loadTemplatesByCategory("stages");
70
+ templates.push(...stageTemplates);
71
+ } else {
72
+ const stageFileName = getStageFileName(stage);
73
+ try {
74
+ const stageTemplate = await loadTemplate("stages", stageFileName);
75
+ templates.push(stageTemplate);
76
+ } catch {
77
+ // Stage template may not exist yet
78
+ }
79
+ }
80
+
81
+ // Compose templates
82
+ let content = composeTemplates(templates, {
83
+ separator: "\n\n---\n\n",
84
+ includeHeaders: false,
85
+ });
86
+
87
+ // Interpolate variables
88
+ if (variables) {
89
+ content = interpolateVariables(content, variables);
90
+ }
91
+
92
+ // Format for target tool
93
+ const stageName = stage === "all" ? undefined : formatStageName(stage);
94
+ return formatForTool(tool, content, { stage: stageName });
95
+ }
96
+
97
+ /**
98
+ * Deploy context files to the target directory.
99
+ */
100
+ export async function deployContext(
101
+ options: ContextBuilderOptions
102
+ ): Promise<DeployResult[]> {
103
+ const { target, stage, format, outputDir, variables } = options;
104
+ const results: DeployResult[] = [];
105
+
106
+ // Determine which tools to deploy to
107
+ const tools: TargetTool[] =
108
+ target === "all" ? getSupportedTools() : [target];
109
+
110
+ // Determine which stages to deploy
111
+ const stages: Stage[] =
112
+ stage === "all"
113
+ ? ["actions", "scripts", "balance", "rebalance"]
114
+ : [stage];
115
+
116
+ for (const tool of tools) {
117
+ if (format === "combined") {
118
+ // Generate single combined file
119
+ try {
120
+ const content = await buildContext(tool, "all", variables);
121
+ const filePath = getOutputPath(tool, format, outputDir);
122
+ const absolutePath = path.resolve(filePath);
123
+
124
+ // Ensure directory exists
125
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
126
+
127
+ // Write file
128
+ await fs.writeFile(absolutePath, content, "utf-8");
129
+
130
+ results.push({
131
+ tool,
132
+ filePath: absolutePath,
133
+ created: true,
134
+ });
135
+ } catch (error) {
136
+ results.push({
137
+ tool,
138
+ filePath: getOutputPath(tool, format, outputDir),
139
+ created: false,
140
+ error: error instanceof Error ? error.message : String(error),
141
+ });
142
+ }
143
+ } else {
144
+ // Generate split files for each stage
145
+ for (const stg of stages) {
146
+ try {
147
+ const content = await buildContext(tool, stg, variables);
148
+ const stageName = getStageFileName(stg);
149
+ const filePath = getOutputPath(tool, format, outputDir, stageName);
150
+ const absolutePath = path.resolve(filePath);
151
+
152
+ // Ensure directory exists
153
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
154
+
155
+ // Write file
156
+ await fs.writeFile(absolutePath, content, "utf-8");
157
+
158
+ results.push({
159
+ tool,
160
+ filePath: absolutePath,
161
+ stage: stg,
162
+ created: true,
163
+ });
164
+ } catch (error) {
165
+ results.push({
166
+ tool,
167
+ filePath: getOutputPath(tool, format, outputDir, stg),
168
+ stage: stg,
169
+ created: false,
170
+ error: error instanceof Error ? error.message : String(error),
171
+ });
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ return results;
178
+ }
179
+
180
+ /**
181
+ * Preview context content without writing files.
182
+ */
183
+ export async function previewContext(
184
+ tool: TargetTool,
185
+ stage: Stage,
186
+ variables?: TemplateVariables
187
+ ): Promise<PreviewResult> {
188
+ const content = await buildContext(tool, stage, variables);
189
+
190
+ return {
191
+ tool,
192
+ stage: stage === "all" ? undefined : stage,
193
+ content,
194
+ };
195
+ }
196
+
197
+ /**
198
+ * List deployed context files in a directory.
199
+ */
200
+ export async function listDeployedContext(
201
+ outputDir: string
202
+ ): Promise<{
203
+ files: Array<{ tool: TargetTool; path: string; exists: boolean }>;
204
+ }> {
205
+ const tools = getSupportedTools();
206
+ const files: Array<{ tool: TargetTool; path: string; exists: boolean }> = [];
207
+
208
+ for (const tool of tools) {
209
+ const config = getToolConfig(tool);
210
+
211
+ // Check combined file
212
+ const combinedPath = path.join(outputDir, config.combinedFile);
213
+ try {
214
+ await fs.access(combinedPath);
215
+ files.push({ tool, path: combinedPath, exists: true });
216
+ } catch {
217
+ files.push({ tool, path: combinedPath, exists: false });
218
+ }
219
+
220
+ // Check split directory
221
+ const splitDir = path.join(outputDir, config.splitDir);
222
+ try {
223
+ const splitFiles = await fs.readdir(splitDir);
224
+ for (const file of splitFiles) {
225
+ if (file.endsWith(config.fileExtension)) {
226
+ files.push({
227
+ tool,
228
+ path: path.join(splitDir, file),
229
+ exists: true,
230
+ });
231
+ }
232
+ }
233
+ } catch {
234
+ // Split directory doesn't exist
235
+ }
236
+ }
237
+
238
+ return { files };
239
+ }
240
+
241
+ /**
242
+ * Remove deployed context files.
243
+ */
244
+ export async function removeContext(
245
+ outputDir: string,
246
+ target: TargetTool | "all"
247
+ ): Promise<Array<{ path: string; removed: boolean; error?: string }>> {
248
+ const tools: TargetTool[] =
249
+ target === "all" ? getSupportedTools() : [target];
250
+ const results: Array<{ path: string; removed: boolean; error?: string }> = [];
251
+
252
+ for (const tool of tools) {
253
+ const config = getToolConfig(tool);
254
+
255
+ // Remove combined file
256
+ const combinedPath = path.join(outputDir, config.combinedFile);
257
+ try {
258
+ await fs.unlink(combinedPath);
259
+ results.push({ path: combinedPath, removed: true });
260
+ } catch (error) {
261
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
262
+ results.push({
263
+ path: combinedPath,
264
+ removed: false,
265
+ error: error instanceof Error ? error.message : String(error),
266
+ });
267
+ }
268
+ }
269
+
270
+ // Remove split directory
271
+ const splitDir = path.join(outputDir, config.splitDir);
272
+ try {
273
+ await fs.rm(splitDir, { recursive: true });
274
+ results.push({ path: splitDir, removed: true });
275
+ } catch (error) {
276
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
277
+ results.push({
278
+ path: splitDir,
279
+ removed: false,
280
+ error: error instanceof Error ? error.message : String(error),
281
+ });
282
+ }
283
+ }
284
+ }
285
+
286
+ return results;
287
+ }
288
+
289
+ /**
290
+ * Get a skill template by name.
291
+ */
292
+ export async function getSkill(name: string): Promise<{
293
+ name: string;
294
+ content: string;
295
+ } | null> {
296
+ try {
297
+ const template = await loadTemplate("skills", name);
298
+ return { name: template.name, content: template.content };
299
+ } catch {
300
+ return null;
301
+ }
302
+ }
303
+
304
+ /**
305
+ * List all available skills.
306
+ */
307
+ export async function listSkills(): Promise<string[]> {
308
+ const templates = await listTemplates();
309
+ return templates.skills;
310
+ }
311
+
312
+ /**
313
+ * Check if templates are available.
314
+ */
315
+ export async function hasTemplates(): Promise<boolean> {
316
+ const templates = await listTemplates();
317
+ return templates.base.length > 0;
318
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * LLM Context Tooling for SceneForge
3
+ *
4
+ * This module provides tools for generating and deploying
5
+ * LLM instruction files for AI coding assistants.
6
+ */
7
+
8
+ // Template loading
9
+ export {
10
+ loadTemplate,
11
+ loadTemplatesByCategory,
12
+ interpolateVariables,
13
+ composeTemplates,
14
+ listTemplates,
15
+ templateExists,
16
+ type LoadedTemplate,
17
+ type TemplateVariables,
18
+ } from "./template-loader.js";
19
+
20
+ // Tool formatting
21
+ export {
22
+ formatForTool,
23
+ getOutputPath,
24
+ getSplitOutputPaths,
25
+ getToolConfig,
26
+ getSupportedTools,
27
+ formatStageName,
28
+ getStageFileName,
29
+ isValidTool,
30
+ isValidFormat,
31
+ TOOL_CONFIGS,
32
+ type TargetTool,
33
+ type DeployFormat,
34
+ type ToolConfig,
35
+ type FormattedOutput,
36
+ } from "./tool-formatter.js";
37
+
38
+ // Context builder
39
+ export {
40
+ buildContext,
41
+ deployContext,
42
+ previewContext,
43
+ listDeployedContext,
44
+ removeContext,
45
+ getSkill,
46
+ listSkills,
47
+ hasTemplates,
48
+ type Stage,
49
+ type ContextBuilderOptions,
50
+ type DeployResult,
51
+ type PreviewResult,
52
+ } from "./context-builder.js";
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Template loading and composition for LLM context files.
3
+ * Loads markdown templates and supports variable interpolation.
4
+ */
5
+
6
+ import * as fs from "fs/promises";
7
+ import * as path from "path";
8
+ import { fileURLToPath } from "url";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ export interface TemplateVariables {
14
+ [key: string]: string | number | boolean | undefined;
15
+ }
16
+
17
+ export interface LoadedTemplate {
18
+ name: string;
19
+ content: string;
20
+ category: "base" | "stages" | "skills";
21
+ }
22
+
23
+ /**
24
+ * Get the templates directory path.
25
+ */
26
+ function getTemplatesDir(): string {
27
+ return path.join(__dirname, "templates");
28
+ }
29
+
30
+ /**
31
+ * Load a single template file by category and name.
32
+ */
33
+ export async function loadTemplate(
34
+ category: "base" | "stages" | "skills",
35
+ name: string
36
+ ): Promise<LoadedTemplate> {
37
+ const templatesDir = getTemplatesDir();
38
+ const filePath = path.join(templatesDir, category, `${name}.md`);
39
+
40
+ try {
41
+ const content = await fs.readFile(filePath, "utf-8");
42
+ return { name, content, category };
43
+ } catch (error) {
44
+ throw new Error(`Failed to load template ${category}/${name}: ${error}`);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Load all templates from a specific category.
50
+ */
51
+ export async function loadTemplatesByCategory(
52
+ category: "base" | "stages" | "skills"
53
+ ): Promise<LoadedTemplate[]> {
54
+ const templatesDir = getTemplatesDir();
55
+ const categoryDir = path.join(templatesDir, category);
56
+
57
+ try {
58
+ const files = await fs.readdir(categoryDir);
59
+ const templates: LoadedTemplate[] = [];
60
+
61
+ for (const file of files) {
62
+ if (file.endsWith(".md")) {
63
+ const name = file.replace(/\.md$/, "");
64
+ const template = await loadTemplate(category, name);
65
+ templates.push(template);
66
+ }
67
+ }
68
+
69
+ return templates;
70
+ } catch (error) {
71
+ throw new Error(`Failed to load templates from ${category}: ${error}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Interpolate variables in template content.
77
+ * Variables use the format: {{variableName}}
78
+ */
79
+ export function interpolateVariables(
80
+ content: string,
81
+ variables: TemplateVariables
82
+ ): string {
83
+ return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
84
+ const value = variables[key];
85
+ if (value === undefined) {
86
+ return match; // Leave unmatched variables as-is
87
+ }
88
+ return String(value);
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Compose multiple templates into a single document.
94
+ */
95
+ export function composeTemplates(
96
+ templates: LoadedTemplate[],
97
+ options?: {
98
+ separator?: string;
99
+ includeHeaders?: boolean;
100
+ }
101
+ ): string {
102
+ const { separator = "\n\n---\n\n", includeHeaders = false } = options ?? {};
103
+
104
+ return templates
105
+ .map((template) => {
106
+ if (includeHeaders) {
107
+ return `<!-- Template: ${template.category}/${template.name} -->\n\n${template.content}`;
108
+ }
109
+ return template.content;
110
+ })
111
+ .join(separator);
112
+ }
113
+
114
+ /**
115
+ * List available templates by category.
116
+ */
117
+ export async function listTemplates(): Promise<{
118
+ base: string[];
119
+ stages: string[];
120
+ skills: string[];
121
+ }> {
122
+ const templatesDir = getTemplatesDir();
123
+ const result: { base: string[]; stages: string[]; skills: string[] } = {
124
+ base: [],
125
+ stages: [],
126
+ skills: [],
127
+ };
128
+
129
+ for (const category of ["base", "stages", "skills"] as const) {
130
+ const categoryDir = path.join(templatesDir, category);
131
+ try {
132
+ const files = await fs.readdir(categoryDir);
133
+ result[category] = files
134
+ .filter((f) => f.endsWith(".md"))
135
+ .map((f) => f.replace(/\.md$/, ""));
136
+ } catch {
137
+ // Directory may not exist yet
138
+ result[category] = [];
139
+ }
140
+ }
141
+
142
+ return result;
143
+ }
144
+
145
+ /**
146
+ * Check if a template exists.
147
+ */
148
+ export async function templateExists(
149
+ category: "base" | "stages" | "skills",
150
+ name: string
151
+ ): Promise<boolean> {
152
+ const templatesDir = getTemplatesDir();
153
+ const filePath = path.join(templatesDir, category, `${name}.md`);
154
+
155
+ try {
156
+ await fs.access(filePath);
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }