sapper-iq 1.1.33 → 1.1.34
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/package.json +1 -1
- package/sapper.mjs +390 -2
package/package.json
CHANGED
package/sapper.mjs
CHANGED
|
@@ -63,8 +63,252 @@ try {
|
|
|
63
63
|
} catch (e) {}
|
|
64
64
|
|
|
65
65
|
const spinner = ora();
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════
|
|
68
|
+
// SAPPER MEMORY FOLDER - All persistent data in one place
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════
|
|
70
|
+
const SAPPER_DIR = '.sapper';
|
|
71
|
+
const CONTEXT_FILE = `${SAPPER_DIR}/context.json`;
|
|
72
|
+
const EMBEDDINGS_FILE = `${SAPPER_DIR}/embeddings.json`;
|
|
73
|
+
const WORKSPACE_FILE = `${SAPPER_DIR}/workspace.json`;
|
|
74
|
+
const CONFIG_FILE = `${SAPPER_DIR}/config.json`;
|
|
75
|
+
|
|
76
|
+
// Ensure .sapper directory exists
|
|
77
|
+
function ensureSapperDir() {
|
|
78
|
+
if (!fs.existsSync(SAPPER_DIR)) {
|
|
79
|
+
fs.mkdirSync(SAPPER_DIR, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Load config (settings like autoAttach)
|
|
84
|
+
function loadConfig() {
|
|
85
|
+
try {
|
|
86
|
+
ensureSapperDir();
|
|
87
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
88
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {}
|
|
91
|
+
return { autoAttach: true }; // Default: auto-attach related files is ON
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function saveConfig(config) {
|
|
95
|
+
ensureSapperDir();
|
|
96
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Global config
|
|
100
|
+
let sapperConfig = loadConfig();
|
|
101
|
+
|
|
102
|
+
// ═══════════════════════════════════════════════════════════════
|
|
103
|
+
// WORKSPACE GRAPH - Track file relationships and summaries
|
|
104
|
+
// ═══════════════════════════════════════════════════════════════
|
|
105
|
+
|
|
106
|
+
function loadWorkspaceGraph() {
|
|
107
|
+
try {
|
|
108
|
+
ensureSapperDir();
|
|
109
|
+
if (fs.existsSync(WORKSPACE_FILE)) {
|
|
110
|
+
return JSON.parse(fs.readFileSync(WORKSPACE_FILE, 'utf8'));
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {}
|
|
113
|
+
return { indexed: null, files: {}, graph: {} };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function saveWorkspaceGraph(workspace) {
|
|
117
|
+
ensureSapperDir();
|
|
118
|
+
fs.writeFileSync(WORKSPACE_FILE, JSON.stringify(workspace, null, 2));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract imports/requires from file content
|
|
122
|
+
function extractDependencies(content, filePath) {
|
|
123
|
+
const deps = new Set();
|
|
124
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
125
|
+
|
|
126
|
+
// JavaScript/TypeScript imports
|
|
127
|
+
if (['js', 'jsx', 'ts', 'tsx', 'mjs'].includes(ext)) {
|
|
128
|
+
// import ... from '...'
|
|
129
|
+
const importMatches = content.matchAll(/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g);
|
|
130
|
+
for (const m of importMatches) deps.add(m[1]);
|
|
131
|
+
|
|
132
|
+
// require('...')
|
|
133
|
+
const requireMatches = content.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g);
|
|
134
|
+
for (const m of requireMatches) deps.add(m[1]);
|
|
135
|
+
|
|
136
|
+
// dynamic import('...')
|
|
137
|
+
const dynImportMatches = content.matchAll(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g);
|
|
138
|
+
for (const m of dynImportMatches) deps.add(m[1]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Python imports
|
|
142
|
+
if (ext === 'py') {
|
|
143
|
+
const fromImports = content.matchAll(/from\s+([.\w]+)\s+import/g);
|
|
144
|
+
for (const m of fromImports) deps.add(m[1]);
|
|
145
|
+
|
|
146
|
+
const imports = content.matchAll(/^import\s+([.\w]+)/gm);
|
|
147
|
+
for (const m of imports) deps.add(m[1]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Filter to only local imports (starting with . or no package scope)
|
|
151
|
+
return Array.from(deps).filter(d => d.startsWith('.') || d.startsWith('/'));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Extract exports from file
|
|
155
|
+
function extractExports(content, filePath) {
|
|
156
|
+
const exports = new Set();
|
|
157
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
158
|
+
|
|
159
|
+
if (['js', 'jsx', 'ts', 'tsx', 'mjs'].includes(ext)) {
|
|
160
|
+
// export function/class/const name
|
|
161
|
+
const namedExports = content.matchAll(/export\s+(?:function|class|const|let|var|async function)\s+(\w+)/g);
|
|
162
|
+
for (const m of namedExports) exports.add(m[1]);
|
|
163
|
+
|
|
164
|
+
// export { name }
|
|
165
|
+
const bracketExports = content.matchAll(/export\s*\{([^}]+)\}/g);
|
|
166
|
+
for (const m of bracketExports) {
|
|
167
|
+
m[1].split(',').forEach(e => {
|
|
168
|
+
const name = e.trim().split(/\s+as\s+/)[0].trim();
|
|
169
|
+
if (name) exports.add(name);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// export default
|
|
174
|
+
if (content.includes('export default')) exports.add('default');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Array.from(exports);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Resolve relative import to actual file path
|
|
181
|
+
function resolveImportPath(importPath, fromFile) {
|
|
182
|
+
if (!importPath.startsWith('.')) return null;
|
|
183
|
+
|
|
184
|
+
const fromDir = dirname(fromFile);
|
|
185
|
+
let resolved = join(fromDir, importPath).replace(/\\/g, '/');
|
|
186
|
+
|
|
187
|
+
// Try common extensions
|
|
188
|
+
const extensions = ['', '.js', '.ts', '.jsx', '.tsx', '.mjs', '/index.js', '/index.ts'];
|
|
189
|
+
for (const ext of extensions) {
|
|
190
|
+
const fullPath = resolved + ext;
|
|
191
|
+
if (fs.existsSync(fullPath)) {
|
|
192
|
+
return fullPath.replace(/^\.\//, '');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Build workspace graph from codebase
|
|
199
|
+
async function buildWorkspaceGraph(showProgress = true) {
|
|
200
|
+
const workspace = { indexed: new Date().toISOString(), files: {}, graph: {} };
|
|
201
|
+
|
|
202
|
+
function scanDir(dir, depth = 0) {
|
|
203
|
+
if (depth > 5) return;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
207
|
+
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const fullPath = dir === '.' ? entry.name : `${dir}/${entry.name}`;
|
|
210
|
+
|
|
211
|
+
if (entry.isDirectory()) {
|
|
212
|
+
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
213
|
+
scanDir(fullPath, depth + 1);
|
|
214
|
+
} else {
|
|
215
|
+
const ext = entry.name.includes('.') ? '.' + entry.name.split('.').pop() : '';
|
|
216
|
+
if (!CODE_EXTENSIONS.has(ext.toLowerCase())) continue;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const stats = fs.statSync(fullPath);
|
|
220
|
+
if (stats.size > MAX_FILE_SIZE) continue;
|
|
221
|
+
|
|
222
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
223
|
+
const deps = extractDependencies(content, fullPath);
|
|
224
|
+
const exports = extractExports(content, fullPath);
|
|
225
|
+
|
|
226
|
+
// Generate brief summary (first meaningful lines)
|
|
227
|
+
const lines = content.split('\n').filter(l => l.trim() && !l.trim().startsWith('//') && !l.trim().startsWith('#'));
|
|
228
|
+
const summary = lines.slice(0, 3).join(' ').substring(0, 150);
|
|
229
|
+
|
|
230
|
+
workspace.files[fullPath] = {
|
|
231
|
+
size: stats.size,
|
|
232
|
+
modified: stats.mtime.toISOString(),
|
|
233
|
+
imports: deps,
|
|
234
|
+
exports: exports,
|
|
235
|
+
summary: summary || '(no summary)'
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Build dependency graph
|
|
239
|
+
workspace.graph[fullPath] = [];
|
|
240
|
+
for (const dep of deps) {
|
|
241
|
+
const resolved = resolveImportPath(dep, fullPath);
|
|
242
|
+
if (resolved) {
|
|
243
|
+
workspace.graph[fullPath].push(resolved);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch (e) {}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (e) {}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
scanDir('.');
|
|
253
|
+
saveWorkspaceGraph(workspace);
|
|
254
|
+
return workspace;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Get related files for a given file (imports + files that import it)
|
|
258
|
+
function getRelatedFiles(filePath, workspace, depth = 1) {
|
|
259
|
+
const related = new Set();
|
|
260
|
+
|
|
261
|
+
// Direct imports
|
|
262
|
+
const imports = workspace.graph[filePath] || [];
|
|
263
|
+
imports.forEach(f => related.add(f));
|
|
264
|
+
|
|
265
|
+
// Files that import this file (reverse lookup)
|
|
266
|
+
for (const [file, deps] of Object.entries(workspace.graph)) {
|
|
267
|
+
if (deps.includes(filePath)) {
|
|
268
|
+
related.add(file);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Second level if depth > 1
|
|
273
|
+
if (depth > 1) {
|
|
274
|
+
const firstLevel = Array.from(related);
|
|
275
|
+
for (const f of firstLevel) {
|
|
276
|
+
const secondImports = workspace.graph[f] || [];
|
|
277
|
+
secondImports.forEach(sf => related.add(sf));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
related.delete(filePath); // Don't include self
|
|
282
|
+
return Array.from(related);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Format workspace summary for AI context
|
|
286
|
+
function formatWorkspaceSummary(workspace) {
|
|
287
|
+
const fileCount = Object.keys(workspace.files).length;
|
|
288
|
+
let output = `\n📊 WORKSPACE INDEX (${fileCount} files)\n`;
|
|
289
|
+
output += '═'.repeat(40) + '\n\n';
|
|
290
|
+
|
|
291
|
+
// Group files by directory
|
|
292
|
+
const byDir = {};
|
|
293
|
+
for (const [path, info] of Object.entries(workspace.files)) {
|
|
294
|
+
const dir = dirname(path) || '.';
|
|
295
|
+
if (!byDir[dir]) byDir[dir] = [];
|
|
296
|
+
byDir[dir].push({ path, ...info });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const [dir, files] of Object.entries(byDir)) {
|
|
300
|
+
output += `📁 ${dir}/\n`;
|
|
301
|
+
for (const f of files.slice(0, 10)) { // Limit per directory
|
|
302
|
+
const name = f.path.split('/').pop();
|
|
303
|
+
const exportList = f.exports?.length ? ` [${f.exports.slice(0, 3).join(', ')}${f.exports.length > 3 ? '...' : ''}]` : '';
|
|
304
|
+
output += ` 📄 ${name}${exportList}\n`;
|
|
305
|
+
}
|
|
306
|
+
if (files.length > 10) output += ` ... and ${files.length - 10} more\n`;
|
|
307
|
+
output += '\n';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return output;
|
|
311
|
+
}
|
|
68
312
|
|
|
69
313
|
// ═══════════════════════════════════════════════════════════════
|
|
70
314
|
// EMBEDDINGS & SEMANTIC SEARCH
|
|
@@ -73,6 +317,7 @@ const EMBEDDINGS_FILE = '.sapper_embeddings.json';
|
|
|
73
317
|
// Load or create embeddings store
|
|
74
318
|
function loadEmbeddings() {
|
|
75
319
|
try {
|
|
320
|
+
ensureSapperDir();
|
|
76
321
|
if (fs.existsSync(EMBEDDINGS_FILE)) {
|
|
77
322
|
return JSON.parse(fs.readFileSync(EMBEDDINGS_FILE, 'utf8'));
|
|
78
323
|
}
|
|
@@ -81,6 +326,7 @@ function loadEmbeddings() {
|
|
|
81
326
|
}
|
|
82
327
|
|
|
83
328
|
function saveEmbeddings(embeddings) {
|
|
329
|
+
ensureSapperDir();
|
|
84
330
|
fs.writeFileSync(EMBEDDINGS_FILE, JSON.stringify(embeddings, null, 2));
|
|
85
331
|
}
|
|
86
332
|
|
|
@@ -662,6 +908,25 @@ async function runSapper() {
|
|
|
662
908
|
// Check for updates
|
|
663
909
|
await checkForUpdates();
|
|
664
910
|
|
|
911
|
+
// Auto-load or build workspace graph
|
|
912
|
+
let workspace = loadWorkspaceGraph();
|
|
913
|
+
if (!workspace.indexed) {
|
|
914
|
+
console.log(chalk.cyan('📊 Building workspace index...'));
|
|
915
|
+
workspace = await buildWorkspaceGraph();
|
|
916
|
+
console.log(chalk.green(`✅ Indexed ${Object.keys(workspace.files).length} files\n`));
|
|
917
|
+
} else {
|
|
918
|
+
const fileCount = Object.keys(workspace.files).length;
|
|
919
|
+
const indexAge = Math.round((Date.now() - new Date(workspace.indexed).getTime()) / 1000 / 60);
|
|
920
|
+
console.log(chalk.gray(`📊 Workspace: ${fileCount} files indexed (${indexAge}m ago)`));
|
|
921
|
+
if (indexAge > 60) {
|
|
922
|
+
console.log(chalk.yellow(` Tip: Run /index to refresh`));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Show memory status
|
|
927
|
+
console.log(chalk.gray(`📁 Memory: .sapper/ folder`));
|
|
928
|
+
console.log(chalk.gray(`🔗 Auto-attach: ${sapperConfig.autoAttach ? 'ON' : 'OFF'} (toggle with /auto)\n`));
|
|
929
|
+
|
|
665
930
|
let messages = [];
|
|
666
931
|
if (fs.existsSync(CONTEXT_FILE)) {
|
|
667
932
|
console.log();
|
|
@@ -675,6 +940,21 @@ async function runSapper() {
|
|
|
675
940
|
console.log(chalk.gray(' ✓ Starting fresh...\n'));
|
|
676
941
|
}
|
|
677
942
|
}
|
|
943
|
+
|
|
944
|
+
// Migrate old files to new .sapper/ folder
|
|
945
|
+
const oldFiles = ['.sapper_context.json', '.sapper_embeddings.json', '.sapper_workspace.json'];
|
|
946
|
+
for (const oldFile of oldFiles) {
|
|
947
|
+
if (fs.existsSync(oldFile)) {
|
|
948
|
+
ensureSapperDir();
|
|
949
|
+
const newFile = `${SAPPER_DIR}/${oldFile.replace('.sapper_', '').replace('_', '.')}`;
|
|
950
|
+
if (!fs.existsSync(newFile)) {
|
|
951
|
+
fs.renameSync(oldFile, newFile);
|
|
952
|
+
console.log(chalk.gray(`📦 Migrated ${oldFile} → ${newFile}`));
|
|
953
|
+
} else {
|
|
954
|
+
fs.unlinkSync(oldFile);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
678
958
|
|
|
679
959
|
let localModels;
|
|
680
960
|
try {
|
|
@@ -824,6 +1104,7 @@ TOOL SYNTAX:
|
|
|
824
1104
|
});
|
|
825
1105
|
|
|
826
1106
|
// 5. Save to context file so it persists
|
|
1107
|
+
ensureSapperDir();
|
|
827
1108
|
fs.writeFileSync(CONTEXT_FILE, JSON.stringify(messages, null, 2));
|
|
828
1109
|
|
|
829
1110
|
console.log(chalk.green(`✅ Pruned context. Sapper reminded to stay in Agent Mode.`));
|
|
@@ -838,6 +1119,9 @@ TOOL SYNTAX:
|
|
|
838
1119
|
`${chalk.cyan('@')} or ${chalk.cyan('/attach')} ${chalk.gray('│')} Pick files to attach (interactive)\n` +
|
|
839
1120
|
`${chalk.cyan('@file')} ${chalk.gray('│')} Attach file inline (e.g., @src/app.js)\n` +
|
|
840
1121
|
`${chalk.cyan('/scan')} ${chalk.gray('│')} Scan codebase into context\n` +
|
|
1122
|
+
`${chalk.cyan('/index')} ${chalk.gray('│')} Rebuild workspace graph\n` +
|
|
1123
|
+
`${chalk.cyan('/graph file')} ${chalk.gray('│')} Show related files\n` +
|
|
1124
|
+
`${chalk.cyan('/auto')} ${chalk.gray('│')} Toggle auto-attach related files\n` +
|
|
841
1125
|
`${chalk.cyan('/recall')} ${chalk.gray('│')} Search memory for relevant context\n` +
|
|
842
1126
|
`${chalk.cyan('/reset /clear')} ${chalk.gray('│')} Clear all context\n` +
|
|
843
1127
|
`${chalk.cyan('/prune')} ${chalk.gray('│')} Save to memory + keep last 4 msgs\n` +
|
|
@@ -850,6 +1134,90 @@ TOOL SYNTAX:
|
|
|
850
1134
|
continue;
|
|
851
1135
|
}
|
|
852
1136
|
|
|
1137
|
+
// Handle index command - rebuild workspace graph
|
|
1138
|
+
if (input.toLowerCase() === '/index') {
|
|
1139
|
+
console.log(chalk.cyan('\n📊 Rebuilding workspace index...'));
|
|
1140
|
+
workspace = await buildWorkspaceGraph();
|
|
1141
|
+
console.log(chalk.green(`✅ Indexed ${Object.keys(workspace.files).length} files`));
|
|
1142
|
+
console.log(chalk.gray(` Graph: ${Object.values(workspace.graph).flat().length} dependencies tracked\n`));
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Handle graph command - show related files
|
|
1147
|
+
if (input.toLowerCase().startsWith('/graph')) {
|
|
1148
|
+
const targetFile = input.slice(6).trim();
|
|
1149
|
+
if (!targetFile) {
|
|
1150
|
+
// Show workspace overview
|
|
1151
|
+
console.log(formatWorkspaceSummary(workspace));
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Find file (support partial match)
|
|
1156
|
+
const matchingFile = Object.keys(workspace.files).find(f =>
|
|
1157
|
+
f === targetFile || f.endsWith('/' + targetFile) || f.endsWith(targetFile)
|
|
1158
|
+
);
|
|
1159
|
+
|
|
1160
|
+
if (!matchingFile) {
|
|
1161
|
+
console.log(chalk.yellow(`File not found in index: ${targetFile}`));
|
|
1162
|
+
console.log(chalk.gray('Tip: Run /index to refresh workspace graph'));
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const fileInfo = workspace.files[matchingFile];
|
|
1167
|
+
const related = getRelatedFiles(matchingFile, workspace);
|
|
1168
|
+
|
|
1169
|
+
console.log();
|
|
1170
|
+
console.log(box(
|
|
1171
|
+
`${chalk.white('File:')} ${chalk.cyan(matchingFile)}\n` +
|
|
1172
|
+
`${chalk.white('Size:')} ${Math.round(fileInfo.size/1024)}KB\n` +
|
|
1173
|
+
`${chalk.white('Exports:')} ${fileInfo.exports?.join(', ') || 'none'}\n` +
|
|
1174
|
+
`${chalk.white('Imports:')} ${fileInfo.imports?.join(', ') || 'none'}\n` +
|
|
1175
|
+
chalk.gray('─'.repeat(40)) + '\n' +
|
|
1176
|
+
`${chalk.white('Related files:')}\n` +
|
|
1177
|
+
(related.length > 0
|
|
1178
|
+
? related.map(r => ` 📄 ${r}`).join('\n')
|
|
1179
|
+
: chalk.gray(' (no related files found)')),
|
|
1180
|
+
'🔗 File Graph', 'cyan'
|
|
1181
|
+
));
|
|
1182
|
+
console.log();
|
|
1183
|
+
|
|
1184
|
+
// Offer to add to context
|
|
1185
|
+
if (related.length > 0) {
|
|
1186
|
+
const addRelated = await safeQuestion(chalk.yellow('Add this file + related to context? ') + chalk.gray('(y/n): '));
|
|
1187
|
+
if (addRelated.toLowerCase() === 'y') {
|
|
1188
|
+
let contextContent = `\n📄 ${matchingFile}:\n`;
|
|
1189
|
+
contextContent += fs.readFileSync(matchingFile, 'utf8');
|
|
1190
|
+
|
|
1191
|
+
for (const relFile of related.slice(0, 5)) { // Limit to 5 related
|
|
1192
|
+
try {
|
|
1193
|
+
contextContent += `\n\n📄 ${relFile} (related):\n`;
|
|
1194
|
+
contextContent += fs.readFileSync(relFile, 'utf8');
|
|
1195
|
+
} catch (e) {}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
messages.push({
|
|
1199
|
+
role: 'user',
|
|
1200
|
+
content: `Here is ${matchingFile} and its related files:\n${contextContent}\n\nUse this context to help me.`
|
|
1201
|
+
});
|
|
1202
|
+
console.log(chalk.green(`✅ Added ${matchingFile} + ${Math.min(related.length, 5)} related files to context`));
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Handle auto-attach toggle
|
|
1209
|
+
if (input.toLowerCase() === '/auto') {
|
|
1210
|
+
sapperConfig.autoAttach = !sapperConfig.autoAttach;
|
|
1211
|
+
saveConfig(sapperConfig);
|
|
1212
|
+
console.log(chalk.cyan(`\n🔗 Auto-attach related files: ${sapperConfig.autoAttach ? chalk.green('ON') : chalk.red('OFF')}`));
|
|
1213
|
+
if (sapperConfig.autoAttach) {
|
|
1214
|
+
console.log(chalk.gray(' When you @file, related imports will be auto-included.'));
|
|
1215
|
+
} else {
|
|
1216
|
+
console.log(chalk.gray(' Only explicitly mentioned files will be attached.'));
|
|
1217
|
+
}
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
853
1221
|
// Handle context size command
|
|
854
1222
|
if (input.toLowerCase() === '/context') {
|
|
855
1223
|
const contextSize = JSON.stringify(messages).length;
|
|
@@ -940,6 +1308,7 @@ TOOL SYNTAX:
|
|
|
940
1308
|
content: `I've scanned the entire codebase. Here are all the files:\n${formattedScan}\n\nYou now have the full codebase context. Use this information to help me.`
|
|
941
1309
|
});
|
|
942
1310
|
|
|
1311
|
+
ensureSapperDir();
|
|
943
1312
|
fs.writeFileSync(CONTEXT_FILE, JSON.stringify(messages, null, 2));
|
|
944
1313
|
console.log(chalk.gray('📝 Codebase added to context. AI now has full picture.\n'));
|
|
945
1314
|
continue;
|
|
@@ -1012,6 +1381,23 @@ TOOL SYNTAX:
|
|
|
1012
1381
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
1013
1382
|
fileAttachments.push({ path: filePath, content, size: stats.size });
|
|
1014
1383
|
console.log(chalk.green(`📎 Attached: ${filePath} (${Math.round(stats.size/1024)}KB)`));
|
|
1384
|
+
|
|
1385
|
+
// Auto-include related files from workspace graph (up to 3) - if enabled
|
|
1386
|
+
if (sapperConfig.autoAttach) {
|
|
1387
|
+
const related = getRelatedFiles(filePath, workspace, 1);
|
|
1388
|
+
for (const relFile of related.slice(0, 3)) {
|
|
1389
|
+
try {
|
|
1390
|
+
if (!fileAttachments.some(f => f.path === relFile)) {
|
|
1391
|
+
const relStats = fs.statSync(relFile);
|
|
1392
|
+
if (relStats.size <= MAX_FILE_SIZE) {
|
|
1393
|
+
const relContent = fs.readFileSync(relFile, 'utf8');
|
|
1394
|
+
fileAttachments.push({ path: relFile, content: relContent, size: relStats.size, related: true });
|
|
1395
|
+
console.log(chalk.gray(` ↳ +${relFile} (related)`));
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
} catch (e) {}
|
|
1399
|
+
}
|
|
1400
|
+
} // end if autoAttach
|
|
1015
1401
|
}
|
|
1016
1402
|
}
|
|
1017
1403
|
} else {
|
|
@@ -1201,6 +1587,7 @@ TOOL SYNTAX:
|
|
|
1201
1587
|
|
|
1202
1588
|
messages.push({ role: 'user', content: `RESULT (${path}): ${result}` });
|
|
1203
1589
|
}
|
|
1590
|
+
ensureSapperDir();
|
|
1204
1591
|
fs.writeFileSync(CONTEXT_FILE, JSON.stringify(messages, null, 2));
|
|
1205
1592
|
|
|
1206
1593
|
if (toolMatches.length > 30) {
|
|
@@ -1216,6 +1603,7 @@ TOOL SYNTAX:
|
|
|
1216
1603
|
});
|
|
1217
1604
|
} else {
|
|
1218
1605
|
// Normal response - save and wait for next input
|
|
1606
|
+
ensureSapperDir();
|
|
1219
1607
|
fs.writeFileSync(CONTEXT_FILE, JSON.stringify(messages, null, 2));
|
|
1220
1608
|
active = false;
|
|
1221
1609
|
spinner.stop(); // Ensure spinner is dead
|