agent-state-machine 2.1.8 → 2.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.
- package/README.md +55 -5
- package/bin/cli.js +1 -140
- package/lib/config-utils.js +259 -0
- package/lib/file-tree.js +366 -0
- package/lib/index.js +109 -2
- package/lib/llm.js +35 -5
- package/lib/runtime/agent.js +146 -118
- package/lib/runtime/model-resolution.js +128 -0
- package/lib/runtime/runtime.js +13 -2
- package/lib/runtime/track-changes.js +252 -0
- package/package.json +1 -1
- package/templates/project-builder/agents/assumptions-clarifier.md +0 -8
- package/templates/project-builder/agents/code-reviewer.md +0 -8
- package/templates/project-builder/agents/code-writer.md +0 -10
- package/templates/project-builder/agents/requirements-clarifier.md +0 -7
- package/templates/project-builder/agents/roadmap-generator.md +0 -10
- package/templates/project-builder/agents/scope-clarifier.md +0 -6
- package/templates/project-builder/agents/security-clarifier.md +0 -9
- package/templates/project-builder/agents/security-reviewer.md +0 -12
- package/templates/project-builder/agents/task-planner.md +0 -10
- package/templates/project-builder/agents/test-planner.md +0 -9
- package/templates/project-builder/config.js +13 -1
- package/templates/starter/config.js +12 -1
- package/vercel-server/public/remote/assets/{index-Cunx4VJE.js → index-BOKpYANC.js} +32 -27
- package/vercel-server/public/remote/assets/index-DHL_iHQW.css +1 -0
- package/vercel-server/public/remote/index.html +2 -2
- package/vercel-server/ui/src/components/ContentCard.jsx +6 -6
- package/vercel-server/public/remote/assets/index-D02x57pS.css +0 -1
package/lib/file-tree.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /lib/file-tree.js
|
|
3
|
+
*
|
|
4
|
+
* File tree tracking utilities for agent-state-machine.
|
|
5
|
+
* Provides Git-based and filesystem-based change detection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { exec } from 'child_process';
|
|
11
|
+
import { promisify } from 'util';
|
|
12
|
+
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
|
|
15
|
+
// Default ignore patterns (mainly for FS fallback, git respects .gitignore)
|
|
16
|
+
export const DEFAULT_IGNORE = [
|
|
17
|
+
'node_modules/**',
|
|
18
|
+
'.git/**',
|
|
19
|
+
'dist/**',
|
|
20
|
+
'build/**',
|
|
21
|
+
'coverage/**',
|
|
22
|
+
'.next/**',
|
|
23
|
+
'.cache/**',
|
|
24
|
+
'workflows/**',
|
|
25
|
+
'*.log',
|
|
26
|
+
'.DS_Store'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Normalize a path for consistent storage.
|
|
31
|
+
* - Converts backslashes to forward slashes
|
|
32
|
+
* - Rejects absolute paths
|
|
33
|
+
* - Rejects traversal outside projectRoot
|
|
34
|
+
*/
|
|
35
|
+
export function normalizePath(relativePath, projectRoot) {
|
|
36
|
+
// Normalize slashes
|
|
37
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
38
|
+
|
|
39
|
+
// Reject absolute paths
|
|
40
|
+
if (path.isAbsolute(normalized)) {
|
|
41
|
+
throw new Error(`Absolute path not allowed: ${normalized}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Reject traversal outside projectRoot
|
|
45
|
+
const resolved = path.resolve(projectRoot, normalized);
|
|
46
|
+
const normalizedRoot = projectRoot.endsWith(path.sep) ? projectRoot : projectRoot + path.sep;
|
|
47
|
+
if (!resolved.startsWith(normalizedRoot) && resolved !== projectRoot.replace(/\/$/, '')) {
|
|
48
|
+
// Also allow exact match with projectRoot
|
|
49
|
+
if (resolved !== projectRoot) {
|
|
50
|
+
throw new Error(`Path traversal outside projectRoot: ${normalized}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a path matches any of the ignore patterns.
|
|
59
|
+
*/
|
|
60
|
+
export function shouldIgnore(relativePath, ignorePatterns = DEFAULT_IGNORE) {
|
|
61
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
62
|
+
|
|
63
|
+
for (const pattern of ignorePatterns) {
|
|
64
|
+
// Simple glob matching
|
|
65
|
+
if (pattern.endsWith('/**')) {
|
|
66
|
+
const prefix = pattern.slice(0, -3);
|
|
67
|
+
if (normalized.startsWith(prefix + '/') || normalized === prefix) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
} else if (pattern.startsWith('*.')) {
|
|
71
|
+
const ext = pattern.slice(1);
|
|
72
|
+
if (normalized.endsWith(ext)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
} else if (normalized === pattern || normalized.startsWith(pattern + '/')) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Git-based change detection
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse git status --porcelain -z output into a Map.
|
|
89
|
+
* Format: XY PATH\0 (or XY ORIG\0PATH\0 for renames)
|
|
90
|
+
*/
|
|
91
|
+
function parseGitStatus(stdout) {
|
|
92
|
+
const files = new Map();
|
|
93
|
+
if (!stdout) return files;
|
|
94
|
+
|
|
95
|
+
const entries = stdout.split('\0').filter(Boolean);
|
|
96
|
+
let i = 0;
|
|
97
|
+
|
|
98
|
+
while (i < entries.length) {
|
|
99
|
+
const entry = entries[i];
|
|
100
|
+
if (entry.length < 3) {
|
|
101
|
+
i++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const statusCode = entry.slice(0, 2);
|
|
106
|
+
const filePath = entry.slice(3);
|
|
107
|
+
|
|
108
|
+
// Handle renames (R) - next entry is the new path
|
|
109
|
+
if (statusCode.startsWith('R') || statusCode.startsWith('C')) {
|
|
110
|
+
i++;
|
|
111
|
+
const newPath = entries[i];
|
|
112
|
+
if (newPath) {
|
|
113
|
+
files.set(newPath, statusCode);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
files.set(filePath, statusCode);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return files;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Capture current git status as a baseline.
|
|
127
|
+
* Returns Map of file paths to their status codes.
|
|
128
|
+
*/
|
|
129
|
+
export async function captureGitBaseline(projectRoot) {
|
|
130
|
+
try {
|
|
131
|
+
// Check if git repo
|
|
132
|
+
await execAsync('git rev-parse --git-dir', { cwd: projectRoot });
|
|
133
|
+
|
|
134
|
+
// Get status: untracked + staged + unstaged
|
|
135
|
+
const { stdout } = await execAsync(
|
|
136
|
+
'git status --porcelain -z',
|
|
137
|
+
{ cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return { files: parseGitStatus(stdout), isGitRepo: true };
|
|
141
|
+
} catch {
|
|
142
|
+
return { files: new Map(), isGitRepo: false };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Detect changes between baseline and current state.
|
|
148
|
+
* Only attributes changes made since baseline capture.
|
|
149
|
+
*/
|
|
150
|
+
export async function detectGitChanges(projectRoot, baseline, ignorePatterns = DEFAULT_IGNORE) {
|
|
151
|
+
const after = await captureGitBaseline(projectRoot);
|
|
152
|
+
|
|
153
|
+
const created = [];
|
|
154
|
+
const modified = [];
|
|
155
|
+
const deleted = [];
|
|
156
|
+
|
|
157
|
+
// Find new and modified files
|
|
158
|
+
for (const [filePath, status] of after.files) {
|
|
159
|
+
if (shouldIgnore(filePath, ignorePatterns)) continue;
|
|
160
|
+
|
|
161
|
+
if (!baseline.files.has(filePath)) {
|
|
162
|
+
created.push(filePath);
|
|
163
|
+
} else if (baseline.files.get(filePath) !== status) {
|
|
164
|
+
modified.push(filePath);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Find deleted files
|
|
169
|
+
for (const [filePath] of baseline.files) {
|
|
170
|
+
if (shouldIgnore(filePath, ignorePatterns)) continue;
|
|
171
|
+
|
|
172
|
+
if (!after.files.has(filePath)) {
|
|
173
|
+
deleted.push(filePath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// MVP: Treat renames as delete+create
|
|
178
|
+
// Future: Use `git diff --name-status -z -M` for rename detection
|
|
179
|
+
return { created, modified, deleted, renamed: [] };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// Filesystem-based change detection (fallback when no git)
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Recursively walk a directory and collect file info.
|
|
188
|
+
*/
|
|
189
|
+
async function walkDirectory(dir, baseDir, ignorePatterns, result = new Map()) {
|
|
190
|
+
let entries;
|
|
191
|
+
try {
|
|
192
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
193
|
+
} catch {
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
const fullPath = path.join(dir, entry.name);
|
|
199
|
+
const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
200
|
+
|
|
201
|
+
if (shouldIgnore(relativePath, ignorePatterns)) continue;
|
|
202
|
+
|
|
203
|
+
if (entry.isDirectory()) {
|
|
204
|
+
await walkDirectory(fullPath, baseDir, ignorePatterns, result);
|
|
205
|
+
} else if (entry.isFile()) {
|
|
206
|
+
try {
|
|
207
|
+
const stats = fs.statSync(fullPath);
|
|
208
|
+
result.set(relativePath, {
|
|
209
|
+
mtime: stats.mtimeMs,
|
|
210
|
+
size: stats.size
|
|
211
|
+
});
|
|
212
|
+
} catch {
|
|
213
|
+
// File may have been deleted between readdir and stat
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Capture filesystem snapshot for change detection.
|
|
223
|
+
*/
|
|
224
|
+
export async function captureFilesystemSnapshot(projectRoot, ignorePatterns = DEFAULT_IGNORE) {
|
|
225
|
+
const files = await walkDirectory(projectRoot, projectRoot, ignorePatterns);
|
|
226
|
+
return { files, isGitRepo: false };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Detect filesystem changes between baseline and current state.
|
|
231
|
+
*/
|
|
232
|
+
export async function detectFilesystemChanges(projectRoot, baseline, ignorePatterns = DEFAULT_IGNORE) {
|
|
233
|
+
const after = await captureFilesystemSnapshot(projectRoot, ignorePatterns);
|
|
234
|
+
|
|
235
|
+
const created = [];
|
|
236
|
+
const modified = [];
|
|
237
|
+
const deleted = [];
|
|
238
|
+
|
|
239
|
+
// Find new and modified files
|
|
240
|
+
for (const [filePath, info] of after.files) {
|
|
241
|
+
const baselineInfo = baseline.files.get(filePath);
|
|
242
|
+
|
|
243
|
+
if (!baselineInfo) {
|
|
244
|
+
created.push(filePath);
|
|
245
|
+
} else if (baselineInfo.mtime !== info.mtime || baselineInfo.size !== info.size) {
|
|
246
|
+
modified.push(filePath);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Find deleted files
|
|
251
|
+
for (const [filePath] of baseline.files) {
|
|
252
|
+
if (!after.files.has(filePath)) {
|
|
253
|
+
deleted.push(filePath);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { created, modified, deleted, renamed: [] };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Unified baseline capture - uses Git if available, falls back to filesystem.
|
|
262
|
+
*/
|
|
263
|
+
export async function captureBaseline(projectRoot, ignorePatterns = DEFAULT_IGNORE) {
|
|
264
|
+
const gitBaseline = await captureGitBaseline(projectRoot);
|
|
265
|
+
|
|
266
|
+
if (gitBaseline.isGitRepo) {
|
|
267
|
+
return gitBaseline;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return captureFilesystemSnapshot(projectRoot, ignorePatterns);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Unified change detection - uses Git if available, falls back to filesystem.
|
|
275
|
+
*/
|
|
276
|
+
export async function detectChanges(projectRoot, baseline, ignorePatterns = DEFAULT_IGNORE) {
|
|
277
|
+
if (baseline.isGitRepo) {
|
|
278
|
+
return detectGitChanges(projectRoot, baseline, ignorePatterns);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return detectFilesystemChanges(projectRoot, baseline, ignorePatterns);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// Export extraction (best-effort)
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Extract exports from JavaScript/TypeScript file content using regex.
|
|
290
|
+
* This is a best-effort extraction, not a full parser.
|
|
291
|
+
*/
|
|
292
|
+
export function extractExportsFromContent(content) {
|
|
293
|
+
if (!content || typeof content !== 'string') return null;
|
|
294
|
+
|
|
295
|
+
const exports = new Set();
|
|
296
|
+
|
|
297
|
+
// export const/let/var/function/class Name
|
|
298
|
+
const namedExportPattern = /export\s+(?:const|let|var|function|class|async\s+function)\s+(\w+)/g;
|
|
299
|
+
let match;
|
|
300
|
+
while ((match = namedExportPattern.exec(content)) !== null) {
|
|
301
|
+
exports.add(match[1]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// export { Name, Name2 as Alias }
|
|
305
|
+
const bracedExportPattern = /export\s*\{([^}]+)\}/g;
|
|
306
|
+
while ((match = bracedExportPattern.exec(content)) !== null) {
|
|
307
|
+
const items = match[1].split(',');
|
|
308
|
+
for (const item of items) {
|
|
309
|
+
const name = item.trim().split(/\s+as\s+/)[0].trim();
|
|
310
|
+
if (name && /^\w+$/.test(name)) {
|
|
311
|
+
exports.add(name);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// export default (tracked as 'default')
|
|
317
|
+
if (/export\s+default\s/.test(content)) {
|
|
318
|
+
exports.add('default');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// module.exports = { name: ... } (CommonJS)
|
|
322
|
+
const cjsPattern = /module\.exports\s*=\s*\{([^}]+)\}/;
|
|
323
|
+
const cjsMatch = content.match(cjsPattern);
|
|
324
|
+
if (cjsMatch) {
|
|
325
|
+
const items = cjsMatch[1].split(',');
|
|
326
|
+
for (const item of items) {
|
|
327
|
+
const name = item.split(':')[0].trim();
|
|
328
|
+
if (name && /^\w+$/.test(name)) {
|
|
329
|
+
exports.add(name);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// module.exports = Name (CommonJS default)
|
|
335
|
+
const cjsDefaultPattern = /module\.exports\s*=\s*(\w+)\s*[;\n]/;
|
|
336
|
+
const cjsDefaultMatch = content.match(cjsDefaultPattern);
|
|
337
|
+
if (cjsDefaultMatch && cjsDefaultMatch[1] !== '{') {
|
|
338
|
+
exports.add('default');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return exports.size > 0 ? Array.from(exports).sort() : null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Extract exports from a file on disk.
|
|
346
|
+
*/
|
|
347
|
+
export function extractExportsFromFile(absolutePath) {
|
|
348
|
+
if (!fs.existsSync(absolutePath)) return null;
|
|
349
|
+
|
|
350
|
+
const ext = path.extname(absolutePath).toLowerCase();
|
|
351
|
+
if (!['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'].includes(ext)) return null;
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
355
|
+
return extractExportsFromContent(content);
|
|
356
|
+
} catch {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============================================================================
|
|
362
|
+
// File tree management (for manual tracking)
|
|
363
|
+
// ============================================================================
|
|
364
|
+
|
|
365
|
+
// These functions require access to the runtime, so they're implemented
|
|
366
|
+
// in track-changes.js and re-exported from index.js
|
package/lib/index.js
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
import { setup } from './setup.js';
|
|
8
8
|
import { llm, llmText, llmJSON, parseJSON, detectAvailableCLIs } from './llm.js';
|
|
9
|
+
import { extractExportsFromContent, extractExportsFromFile } from './file-tree.js';
|
|
10
|
+
import {
|
|
11
|
+
trackFile as trackFileInternal,
|
|
12
|
+
getFileTree as getFileTreeInternal,
|
|
13
|
+
untrackFile as untrackFileInternal
|
|
14
|
+
} from './runtime/track-changes.js';
|
|
9
15
|
|
|
10
16
|
import {
|
|
11
17
|
WorkflowRuntime,
|
|
@@ -83,6 +89,97 @@ export const memory = new Proxy(
|
|
|
83
89
|
}
|
|
84
90
|
);
|
|
85
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Live fileTree proxy:
|
|
94
|
+
* - Reads/writes target the current workflow runtime's fileTree in memory
|
|
95
|
+
* - Indexed by relative file path from projectRoot
|
|
96
|
+
*/
|
|
97
|
+
export const fileTree = new Proxy(
|
|
98
|
+
{},
|
|
99
|
+
{
|
|
100
|
+
get(_target, prop) {
|
|
101
|
+
const runtime = getCurrentRuntime();
|
|
102
|
+
if (!runtime) return undefined;
|
|
103
|
+
return runtime._rawMemory.fileTree?.[prop];
|
|
104
|
+
},
|
|
105
|
+
set(_target, prop, value) {
|
|
106
|
+
const runtime = getCurrentRuntime();
|
|
107
|
+
if (!runtime) {
|
|
108
|
+
throw new Error('fileTree can only be mutated within a running workflow');
|
|
109
|
+
}
|
|
110
|
+
if (!runtime._rawMemory.fileTree) {
|
|
111
|
+
runtime._rawMemory.fileTree = {};
|
|
112
|
+
}
|
|
113
|
+
runtime._rawMemory.fileTree[prop] = value;
|
|
114
|
+
runtime.memory.fileTree = runtime._rawMemory.fileTree;
|
|
115
|
+
return true;
|
|
116
|
+
},
|
|
117
|
+
deleteProperty(_target, prop) {
|
|
118
|
+
const runtime = getCurrentRuntime();
|
|
119
|
+
if (!runtime) {
|
|
120
|
+
throw new Error('fileTree can only be mutated within a running workflow');
|
|
121
|
+
}
|
|
122
|
+
if (runtime._rawMemory.fileTree) {
|
|
123
|
+
delete runtime._rawMemory.fileTree[prop];
|
|
124
|
+
runtime.memory.fileTree = runtime._rawMemory.fileTree;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
has(_target, prop) {
|
|
129
|
+
const runtime = getCurrentRuntime();
|
|
130
|
+
return runtime?._rawMemory.fileTree?.hasOwnProperty(prop) ?? false;
|
|
131
|
+
},
|
|
132
|
+
ownKeys() {
|
|
133
|
+
const runtime = getCurrentRuntime();
|
|
134
|
+
return Object.keys(runtime?._rawMemory.fileTree || {});
|
|
135
|
+
},
|
|
136
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
137
|
+
const runtime = getCurrentRuntime();
|
|
138
|
+
if (runtime?._rawMemory.fileTree?.hasOwnProperty(prop)) {
|
|
139
|
+
return {
|
|
140
|
+
configurable: true,
|
|
141
|
+
enumerable: true,
|
|
142
|
+
value: runtime._rawMemory.fileTree[prop]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Track a file in the fileTree.
|
|
152
|
+
* @param {string} relativePath - Path relative to projectRoot
|
|
153
|
+
* @param {object} options - Tracking options (caption, exports, extractExports, metadata, agentName)
|
|
154
|
+
*/
|
|
155
|
+
export function trackFile(relativePath, options = {}) {
|
|
156
|
+
const runtime = getCurrentRuntime();
|
|
157
|
+
if (!runtime) {
|
|
158
|
+
throw new Error('trackFile() must be called within a workflow context');
|
|
159
|
+
}
|
|
160
|
+
return trackFileInternal(runtime, relativePath, options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get all tracked files.
|
|
165
|
+
*/
|
|
166
|
+
export function getFileTree() {
|
|
167
|
+
const runtime = getCurrentRuntime();
|
|
168
|
+
if (!runtime) return {};
|
|
169
|
+
return getFileTreeInternal(runtime);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Remove a file from tracking.
|
|
174
|
+
*/
|
|
175
|
+
export function untrackFile(relativePath) {
|
|
176
|
+
const runtime = getCurrentRuntime();
|
|
177
|
+
if (!runtime) {
|
|
178
|
+
throw new Error('untrackFile() must be called within a workflow context');
|
|
179
|
+
}
|
|
180
|
+
return untrackFileInternal(runtime, relativePath);
|
|
181
|
+
}
|
|
182
|
+
|
|
86
183
|
export {
|
|
87
184
|
setup,
|
|
88
185
|
llm,
|
|
@@ -108,7 +205,10 @@ export {
|
|
|
108
205
|
parallel,
|
|
109
206
|
parallelLimit,
|
|
110
207
|
getCurrentRuntime,
|
|
111
|
-
getMemory
|
|
208
|
+
getMemory,
|
|
209
|
+
// File tree utilities
|
|
210
|
+
extractExportsFromContent,
|
|
211
|
+
extractExportsFromFile
|
|
112
212
|
};
|
|
113
213
|
|
|
114
214
|
const api = {
|
|
@@ -137,7 +237,14 @@ const api = {
|
|
|
137
237
|
parallelLimit,
|
|
138
238
|
getCurrentRuntime,
|
|
139
239
|
getMemory,
|
|
140
|
-
memory
|
|
240
|
+
memory,
|
|
241
|
+
// File tree
|
|
242
|
+
fileTree,
|
|
243
|
+
trackFile,
|
|
244
|
+
getFileTree,
|
|
245
|
+
untrackFile,
|
|
246
|
+
extractExportsFromContent,
|
|
247
|
+
extractExportsFromFile
|
|
141
248
|
};
|
|
142
249
|
|
|
143
250
|
export default api;
|
package/lib/llm.js
CHANGED
|
@@ -7,6 +7,8 @@ import path from 'path';
|
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import { spawn, execSync } from 'child_process';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
|
+
import { getCurrentRuntime } from './runtime/runtime.js';
|
|
11
|
+
import { resolveUnknownModel } from './runtime/model-resolution.js';
|
|
10
12
|
|
|
11
13
|
const require = createRequire(import.meta.url);
|
|
12
14
|
|
|
@@ -160,6 +162,18 @@ export function buildPrompt(context, options) {
|
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
// Add file tracking context if enabled
|
|
166
|
+
if (context._config?.fileTracking !== false && context._config?.projectRoot) {
|
|
167
|
+
parts.push('# File Context\n\n');
|
|
168
|
+
parts.push(`You are running from: ${context._config.workflowDir}\n`);
|
|
169
|
+
parts.push(`Project root (where to create files): ${context._config.projectRoot}\n\n`);
|
|
170
|
+
parts.push('When creating or modifying files, use paths relative to the project root.\n');
|
|
171
|
+
parts.push('To annotate files, include `_files` in your JSON response:\n');
|
|
172
|
+
parts.push('```json\n');
|
|
173
|
+
parts.push('{ "_files": [{ "path": "src/file.js", "caption": "Brief description" }] }\n');
|
|
174
|
+
parts.push('```\n---\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
163
177
|
return parts.join('\n');
|
|
164
178
|
}
|
|
165
179
|
|
|
@@ -419,13 +433,29 @@ export async function llm(context, options) {
|
|
|
419
433
|
const apiKeys = config.apiKeys || {};
|
|
420
434
|
|
|
421
435
|
// Look up the model command/config
|
|
422
|
-
|
|
436
|
+
let modelConfig = models[options.model];
|
|
423
437
|
|
|
424
438
|
if (!modelConfig) {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
439
|
+
const runtime = getCurrentRuntime();
|
|
440
|
+
if (runtime) {
|
|
441
|
+
const workflowDir = options.workflowDir || config.workflowDir || runtime.workflowDir;
|
|
442
|
+
if (!workflowDir) {
|
|
443
|
+
throw new Error(`Unknown model key: "${options.model}". Workflow directory is missing.`);
|
|
444
|
+
}
|
|
445
|
+
modelConfig = await resolveUnknownModel(options.model, config, workflowDir, {
|
|
446
|
+
availableCLIs: detectAvailableCLIs()
|
|
447
|
+
});
|
|
448
|
+
if (!config.models) {
|
|
449
|
+
config.models = models;
|
|
450
|
+
}
|
|
451
|
+
config.models[options.model] = modelConfig;
|
|
452
|
+
runtime.workflowConfig.models[options.model] = modelConfig;
|
|
453
|
+
} else {
|
|
454
|
+
const available = Object.keys(models).join(', ');
|
|
455
|
+
throw new Error(
|
|
456
|
+
`Unknown model key: "${options.model}". Available models: ${available || 'none defined'}`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
429
459
|
}
|
|
430
460
|
|
|
431
461
|
// Build the full prompt
|