@zoebuildsai/trace 1.5.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.
Files changed (130) hide show
  1. package/.gitignore +115 -0
  2. package/.trace/progress.json +22 -0
  3. package/README.md +466 -0
  4. package/RELEASE-NOTES-1.5.0.md +410 -0
  5. package/STATUS.md +245 -0
  6. package/dist/auto-commit.d.ts +66 -0
  7. package/dist/auto-commit.d.ts.map +1 -0
  8. package/dist/auto-commit.js +180 -0
  9. package/dist/auto-commit.js.map +1 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +246 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands.d.ts +46 -0
  15. package/dist/commands.d.ts.map +1 -0
  16. package/dist/commands.js +256 -0
  17. package/dist/commands.js.map +1 -0
  18. package/dist/diff.d.ts +23 -0
  19. package/dist/diff.d.ts.map +1 -0
  20. package/dist/diff.js +106 -0
  21. package/dist/diff.js.map +1 -0
  22. package/dist/github.d.ts.map +1 -0
  23. package/dist/github.js.map +1 -0
  24. package/dist/index-cache.d.ts +35 -0
  25. package/dist/index-cache.d.ts.map +1 -0
  26. package/dist/index-cache.js +114 -0
  27. package/dist/index-cache.js.map +1 -0
  28. package/dist/index.d.ts +15 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +25 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/storage.d.ts +45 -0
  33. package/dist/storage.d.ts.map +1 -0
  34. package/dist/storage.js +151 -0
  35. package/dist/storage.js.map +1 -0
  36. package/dist/sync.d.ts +60 -0
  37. package/dist/sync.js +184 -0
  38. package/dist/tags.d.ts +85 -0
  39. package/dist/tags.d.ts.map +1 -0
  40. package/dist/tags.js +219 -0
  41. package/dist/tags.js.map +1 -0
  42. package/dist/types.d.ts +102 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +6 -0
  45. package/dist/types.js.map +1 -0
  46. package/docs/.nojekyll +0 -0
  47. package/docs/README.md +73 -0
  48. package/docs/_config.yml +2 -0
  49. package/docs/index.html +960 -0
  50. package/docs-website/package.json +20 -0
  51. package/jest.config.js +21 -0
  52. package/package.json +50 -0
  53. package/scripts/init.ts +290 -0
  54. package/src/agent-audit.ts +270 -0
  55. package/src/agent-checkout.ts +227 -0
  56. package/src/agent-coordination.ts +318 -0
  57. package/src/async-queue.ts +203 -0
  58. package/src/auto-branching.ts +279 -0
  59. package/src/auto-commit.ts +166 -0
  60. package/src/cherry-pick.ts +252 -0
  61. package/src/chunked-upload.ts +224 -0
  62. package/src/cli-v2.ts +335 -0
  63. package/src/cli.ts +318 -0
  64. package/src/cliff-detection.ts +232 -0
  65. package/src/commands.ts +267 -0
  66. package/src/commit-hash-system.ts +351 -0
  67. package/src/compression.ts +176 -0
  68. package/src/conflict-resolution-ui.ts +277 -0
  69. package/src/conflict-visualization.ts +238 -0
  70. package/src/diff-formatter.ts +184 -0
  71. package/src/diff.ts +124 -0
  72. package/src/distributed-coordination.ts +273 -0
  73. package/src/git-interop.ts +316 -0
  74. package/src/index-cache.ts +88 -0
  75. package/src/index.ts +38 -0
  76. package/src/merge-engine.ts +143 -0
  77. package/src/message-search.ts +370 -0
  78. package/src/performance-monitoring.ts +236 -0
  79. package/src/rebase.ts +327 -0
  80. package/src/rollback.ts +215 -0
  81. package/src/semantic-grouping.ts +245 -0
  82. package/src/stage-area.ts +324 -0
  83. package/src/stash.ts +278 -0
  84. package/src/storage.ts +131 -0
  85. package/src/sync.ts +205 -0
  86. package/src/tags.ts +244 -0
  87. package/src/types.ts +119 -0
  88. package/src/webhooks.ts +119 -0
  89. package/src/workspace-isolation.ts +298 -0
  90. package/tests/auto-commit.test.ts +308 -0
  91. package/tests/checkout.test.ts +136 -0
  92. package/tests/commit.test.ts +118 -0
  93. package/tests/diff.test.ts +191 -0
  94. package/tests/github.test.ts +94 -0
  95. package/tests/integration.test.ts +267 -0
  96. package/tests/log.test.ts +125 -0
  97. package/tests/phase2-integration.test.ts +370 -0
  98. package/tests/storage.test.ts +167 -0
  99. package/tests/tags.test.ts +477 -0
  100. package/tests/types.test.ts +75 -0
  101. package/tests/v1.1/agent-audit.test.ts +472 -0
  102. package/tests/v1.1/agent-coordination.test.ts +308 -0
  103. package/tests/v1.1/async-queue.test.ts +253 -0
  104. package/tests/v1.1/comprehensive.test.ts +521 -0
  105. package/tests/v1.1/diff-formatter.test.ts +238 -0
  106. package/tests/v1.1/integration.test.ts +389 -0
  107. package/tests/v1.1/onboarding.test.ts +365 -0
  108. package/tests/v1.1/rollback.test.ts +370 -0
  109. package/tests/v1.1/semantic-grouping.test.ts +230 -0
  110. package/tests/v1.2/chunked-upload.test.ts +301 -0
  111. package/tests/v1.2/cliff-detection.test.ts +272 -0
  112. package/tests/v1.2/commit-hash-system.test.ts +288 -0
  113. package/tests/v1.2/compression.test.ts +220 -0
  114. package/tests/v1.2/conflict-visualization.test.ts +263 -0
  115. package/tests/v1.2/distributed.test.ts +261 -0
  116. package/tests/v1.2/performance-monitoring.test.ts +328 -0
  117. package/tests/v1.3/auto-branching.test.ts +270 -0
  118. package/tests/v1.3/message-search.test.ts +264 -0
  119. package/tests/v1.3/stage-area.test.ts +330 -0
  120. package/tests/v1.3/stash-rebase-cherry-pick.test.ts +361 -0
  121. package/tests/v1.4/cli.test.ts +171 -0
  122. package/tests/v1.4/conflict-resolution-advanced.test.ts +429 -0
  123. package/tests/v1.4/conflict-resolution-ui.test.ts +286 -0
  124. package/tests/v1.4/workspace-isolation-advanced.test.ts +382 -0
  125. package/tests/v1.4/workspace-isolation.test.ts +268 -0
  126. package/tests/v1.5/agent-coordination.real.test.ts +401 -0
  127. package/tests/v1.5/cli-v2.test.ts +354 -0
  128. package/tests/v1.5/git-interop.real.test.ts +358 -0
  129. package/tests/v1.5/integration-testing.real.test.ts +440 -0
  130. package/tsconfig.json +26 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Cliff Detection for Trace
3
+ * Warn about breaking changes before checkout/merge
4
+ */
5
+
6
+ export interface CliffWarning {
7
+ type: 'breaking-change' | 'api-removal' | 'security-issue' | 'performance-regression';
8
+ severity: 'low' | 'medium' | 'high' | 'critical';
9
+ description: string;
10
+ affectedFiles: string[];
11
+ suggestion: string;
12
+ }
13
+
14
+ export interface CliffAnalysis {
15
+ hasCliffs: boolean;
16
+ warnings: CliffWarning[];
17
+ safeToUpdate: boolean;
18
+ riskScore: number; // 0-100
19
+ }
20
+
21
+ export class CliffDetection {
22
+ // Common breaking change patterns
23
+ private static readonly BREAKING_PATTERNS = [
24
+ { pattern: /export (class|interface|type) \w+/g, label: 'Public API definition' },
25
+ { pattern: /async (function|\w+\()/g, label: 'Async function signature' },
26
+ { pattern: /constructor\(/g, label: 'Class constructor' },
27
+ { pattern: /throw new Error/g, label: 'Error throwing' },
28
+ ];
29
+
30
+ // API removal patterns
31
+ private static readonly REMOVAL_PATTERNS = [
32
+ { pattern: /deprecated/i, label: 'Deprecated API' },
33
+ { pattern: /removed in v\d+/i, label: 'Removal notice' },
34
+ { pattern: /no longer supported/i, label: 'Unsupported feature' },
35
+ ];
36
+
37
+ // Security issue patterns
38
+ private static readonly SECURITY_PATTERNS = [
39
+ { pattern: /TODO.*security/i, label: 'Security TODO' },
40
+ { pattern: /FIXME.*auth/i, label: 'Auth issue' },
41
+ { pattern: /vulnerable|exploit|vulnerability/i, label: 'Known vulnerability' },
42
+ ];
43
+
44
+ /**
45
+ * Analyze cliff risks between two versions
46
+ */
47
+ static analyzeCliff(
48
+ oldFiles: Map<string, string>,
49
+ newFiles: Map<string, string>
50
+ ): CliffAnalysis {
51
+ const warnings: CliffWarning[] = [];
52
+ let riskScore = 0;
53
+
54
+ // Check for removed files (major breaking change)
55
+ for (const [file] of oldFiles) {
56
+ if (!newFiles.has(file)) {
57
+ warnings.push({
58
+ type: 'breaking-change',
59
+ severity: 'high',
60
+ description: `File removed: ${file}`,
61
+ affectedFiles: [file],
62
+ suggestion: `Check if ${file} is still needed. If yes, restore from previous version.`,
63
+ });
64
+ riskScore += 25;
65
+ }
66
+ }
67
+
68
+ // Check for breaking changes in files
69
+ for (const [file, newContent] of newFiles) {
70
+ const oldContent = oldFiles.get(file);
71
+ if (!oldContent) continue; // New file, not a breaking change
72
+
73
+ const oldCliffs = this.findBreakingPatterns(oldContent, file);
74
+ const newCliffs = this.findBreakingPatterns(newContent, file);
75
+
76
+ // Check if breaking patterns were removed
77
+ if (oldCliffs.length > newCliffs.length) {
78
+ warnings.push({
79
+ type: 'api-removal',
80
+ severity: 'high',
81
+ description: `Public API removed from ${file}`,
82
+ affectedFiles: [file],
83
+ suggestion: `Review what was removed. Update code that depends on removed APIs.`,
84
+ });
85
+ riskScore += 20;
86
+ }
87
+
88
+ // Check for security issues
89
+ const securityWarnings = this.findSecurityIssues(newContent, file);
90
+ warnings.push(...securityWarnings);
91
+ riskScore += securityWarnings.length * 15;
92
+
93
+ // Check for performance regressions
94
+ if (this.hasPerformanceRegression(oldContent, newContent)) {
95
+ warnings.push({
96
+ type: 'performance-regression',
97
+ severity: 'medium',
98
+ description: `Potential performance regression in ${file}`,
99
+ affectedFiles: [file],
100
+ suggestion: `Run benchmarks before deploying to production.`,
101
+ });
102
+ riskScore += 10;
103
+ }
104
+ }
105
+
106
+ // Cap risk score at 100
107
+ riskScore = Math.min(riskScore, 100);
108
+
109
+ return {
110
+ hasCliffs: warnings.length > 0,
111
+ warnings,
112
+ safeToUpdate: warnings.filter(w => w.severity === 'critical').length === 0,
113
+ riskScore,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Find breaking patterns in code
119
+ */
120
+ private static findBreakingPatterns(content: string, file: string): string[] {
121
+ const patterns: string[] = [];
122
+
123
+ for (const { pattern, label } of this.BREAKING_PATTERNS) {
124
+ const matches = content.match(pattern);
125
+ if (matches) {
126
+ for (const match of matches) {
127
+ patterns.push(`${label}: ${match}`);
128
+ }
129
+ }
130
+ }
131
+
132
+ return patterns;
133
+ }
134
+
135
+ /**
136
+ * Find security issues
137
+ */
138
+ private static findSecurityIssues(content: string, file: string): CliffWarning[] {
139
+ const warnings: CliffWarning[] = [];
140
+
141
+ for (const { pattern, label } of this.SECURITY_PATTERNS) {
142
+ if (pattern.test(content)) {
143
+ warnings.push({
144
+ type: 'security-issue',
145
+ severity: 'critical',
146
+ description: `${label} detected in ${file}`,
147
+ affectedFiles: [file],
148
+ suggestion: `Fix security issue before deploying.`,
149
+ });
150
+ }
151
+ }
152
+
153
+ return warnings;
154
+ }
155
+
156
+ /**
157
+ * Detect performance regressions
158
+ */
159
+ private static hasPerformanceRegression(oldContent: string, newContent: string): boolean {
160
+ const oldLoops = (oldContent.match(/for|while|forEach|map|reduce/g) || []).length;
161
+ const newLoops = (newContent.match(/for|while|forEach|map|reduce/g) || []).length;
162
+
163
+ // Significant increase in loops might indicate regression
164
+ if (newLoops > oldLoops * 2) {
165
+ return true;
166
+ }
167
+
168
+ // Check for new synchronous operations
169
+ if (newContent.includes('sync') && !oldContent.includes('sync')) {
170
+ return true;
171
+ }
172
+
173
+ return false;
174
+ }
175
+
176
+ /**
177
+ * Get risk level from score
178
+ */
179
+ static getRiskLevel(score: number): 'safe' | 'caution' | 'warning' | 'dangerous' {
180
+ if (score < 20) return 'safe';
181
+ if (score < 50) return 'caution';
182
+ if (score < 80) return 'warning';
183
+ return 'dangerous';
184
+ }
185
+
186
+ /**
187
+ * Format warnings for human review
188
+ */
189
+ static formatWarnings(analysis: CliffAnalysis): string {
190
+ let output = `\n🏔️ CLIFF DETECTION REPORT\n`;
191
+ output += `Risk Level: ${this.getRiskLevel(analysis.riskScore).toUpperCase()} (${analysis.riskScore}/100)\n`;
192
+ output += `Safe to update: ${analysis.safeToUpdate ? '✅ YES' : '❌ NO'}\n\n`;
193
+
194
+ if (analysis.warnings.length === 0) {
195
+ output += `No breaking changes detected. Safe to update. 🎉\n`;
196
+ return output;
197
+ }
198
+
199
+ output += `${analysis.warnings.length} warning(s) found:\n`;
200
+ for (let i = 0; i < analysis.warnings.length; i++) {
201
+ const w = analysis.warnings[i];
202
+ output += `\n${i + 1}. [${w.severity.toUpperCase()}] ${w.type}\n`;
203
+ output += ` Description: ${w.description}\n`;
204
+ output += ` Files: ${w.affectedFiles.join(', ')}\n`;
205
+ output += ` Action: ${w.suggestion}\n`;
206
+ }
207
+
208
+ return output;
209
+ }
210
+
211
+ /**
212
+ * Pre-checkout warning
213
+ */
214
+ static checkBeforeCheckout(
215
+ currentFiles: Map<string, string>,
216
+ targetFiles: Map<string, string>
217
+ ): CliffAnalysis {
218
+ return this.analyzeCliff(currentFiles, targetFiles);
219
+ }
220
+
221
+ /**
222
+ * Pre-merge warning
223
+ */
224
+ static checkBeforeMerge(
225
+ baseFiles: Map<string, string>,
226
+ mergeFiles: Map<string, string>
227
+ ): CliffAnalysis {
228
+ return this.analyzeCliff(baseFiles, mergeFiles);
229
+ }
230
+ }
231
+
232
+ export default CliffDetection;
@@ -0,0 +1,267 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { Storage } from './storage';
4
+ import { Differ } from './diff';
5
+ import { IndexCache } from './index-cache';
6
+ import {
7
+ CommitObject,
8
+ TreeObject,
9
+ FileEntry,
10
+ StatusResult,
11
+ CommitOptions,
12
+ LogEntry,
13
+ CheckoutOptions,
14
+ DiffOptions,
15
+ } from './types';
16
+
17
+ export class TraceCommands {
18
+ private storage: Storage;
19
+ private differ: Differ;
20
+ private indexCache: IndexCache;
21
+ private currentCommit: string | null = null;
22
+
23
+ constructor(basePath: string = path.join(process.env.HOME || '', '.openclaw/memory-git')) {
24
+ this.storage = new Storage(basePath);
25
+ this.differ = new Differ(this.storage);
26
+ this.indexCache = new IndexCache(this.storage);
27
+ this.loadCurrentCommit();
28
+ }
29
+
30
+ private loadCurrentCommit(): void {
31
+ const ref = this.storage.loadRef('HEAD');
32
+ this.currentCommit = ref;
33
+ }
34
+
35
+ /**
36
+ * memory commit <message>
37
+ */
38
+ commit(message: string, author: string = 'agent', metadata?: Record<string, unknown>): string {
39
+ const startTime = Date.now();
40
+
41
+ // Load current commit (parent)
42
+ let parent: CommitObject | null = null;
43
+ if (this.currentCommit) {
44
+ parent = this.storage.loadCommit(this.currentCommit);
45
+ }
46
+
47
+ // Create tree for new commit
48
+ const files = this.readWorkingDirectory(parent?.tree);
49
+ const tree: TreeObject = {
50
+ files,
51
+ hash: '',
52
+ };
53
+
54
+ // Hash tree content
55
+ tree.hash = this.storage.hash(JSON.stringify(this.serializeTree(tree)));
56
+
57
+ // Create commit object
58
+ const commit: CommitObject = {
59
+ hash: '',
60
+ message,
61
+ timestamp: Date.now(),
62
+ author,
63
+ parent: this.currentCommit,
64
+ tree,
65
+ metadata,
66
+ };
67
+
68
+ // Hash commit
69
+ const commitContent = JSON.stringify({
70
+ message: commit.message,
71
+ timestamp: commit.timestamp,
72
+ author: commit.author,
73
+ parent: commit.parent,
74
+ tree: commit.tree.hash,
75
+ });
76
+ commit.hash = this.storage.hash(commitContent);
77
+
78
+ // Save to storage
79
+ this.storage.saveCommit(commit);
80
+ this.storage.saveRef('HEAD', commit.hash);
81
+ this.currentCommit = commit.hash;
82
+
83
+ const elapsed = Date.now() - startTime;
84
+ console.log(`[commit] ${commit.hash.slice(0, 8)} - ${elapsed}ms`);
85
+
86
+ return commit.hash;
87
+ }
88
+
89
+ /**
90
+ * memory log [--limit N]
91
+ */
92
+ log(limit?: number): LogEntry[] {
93
+ if (!this.currentCommit) {
94
+ return [];
95
+ }
96
+
97
+ const history = this.storage.getHistory(this.currentCommit);
98
+ const entries: LogEntry[] = history.map(c => ({
99
+ hash: c.hash,
100
+ shortHash: c.hash.slice(0, 8),
101
+ message: c.message,
102
+ timestamp: c.timestamp,
103
+ author: c.author,
104
+ }));
105
+
106
+ if (limit && limit > 0) {
107
+ return entries.slice(0, limit);
108
+ }
109
+
110
+ return entries;
111
+ }
112
+
113
+ /**
114
+ * memory diff <commit1> <commit2>
115
+ */
116
+ diff(hash1: string, hash2: string, options?: DiffOptions): string {
117
+ const commit1 = this.storage.loadCommit(hash1);
118
+ const commit2 = this.storage.loadCommit(hash2);
119
+
120
+ if (!commit1 || !commit2) {
121
+ throw new Error('One or both commits not found');
122
+ }
123
+
124
+ const result = this.differ.diff(commit1.tree, commit2.tree, options);
125
+ return this.differ.format(result);
126
+ }
127
+
128
+ /**
129
+ * memory checkout <commit>
130
+ */
131
+ checkout(hash: string, options?: CheckoutOptions): void {
132
+ const startTime = Date.now();
133
+ const commit = this.storage.loadCommit(hash);
134
+
135
+ if (!commit) {
136
+ throw new Error(`Commit ${hash} not found`);
137
+ }
138
+
139
+ // Restore files from commit
140
+ commit.tree.files.forEach(entry => {
141
+ if (entry.mode === 'file') {
142
+ // In production: read from blob storage
143
+ const filePath = path.join(process.env.HOME || '', '.openclaw/memory', entry.path);
144
+ const dir = path.dirname(filePath);
145
+ if (!fs.existsSync(dir)) {
146
+ fs.mkdirSync(dir, { recursive: true });
147
+ }
148
+ fs.writeFileSync(filePath, `[blob:${entry.hash}]`);
149
+ }
150
+ });
151
+
152
+ this.storage.saveRef('HEAD', hash);
153
+ this.currentCommit = hash;
154
+ this.indexCache.invalidate(hash);
155
+
156
+ const elapsed = Date.now() - startTime;
157
+ console.log(`[checkout] ${hash.slice(0, 8)} - ${elapsed}ms`);
158
+ }
159
+
160
+ /**
161
+ * memory status
162
+ */
163
+ status(): StatusResult {
164
+ if (!this.currentCommit) {
165
+ return {
166
+ modified: [],
167
+ added: [],
168
+ deleted: [],
169
+ untracked: [],
170
+ clean: true,
171
+ };
172
+ }
173
+
174
+ const currentCommit = this.storage.loadCommit(this.currentCommit);
175
+ if (!currentCommit) {
176
+ throw new Error('Current commit not found');
177
+ }
178
+
179
+ const workingTree = this.readWorkingDirectory();
180
+ const diff = this.differ.diff(currentCommit.tree, workingTree);
181
+
182
+ return {
183
+ modified: Array.from(diff.modified.keys()),
184
+ added: Array.from(diff.added.keys()),
185
+ deleted: Array.from(diff.deleted.keys()),
186
+ untracked: this.findUntracked(currentCommit.tree, workingTree),
187
+ clean: diff.stats.totalChanges === 0,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Get current HEAD commit
193
+ */
194
+ getCurrentCommit(): CommitObject | null {
195
+ if (!this.currentCommit) {
196
+ return null;
197
+ }
198
+ return this.storage.loadCommit(this.currentCommit);
199
+ }
200
+
201
+ /**
202
+ * Read working directory and create tree
203
+ */
204
+ private readWorkingDirectory(baseTree?: TreeObject): Map<string, FileEntry> {
205
+ const memoryDir = path.join(process.env.HOME || '', '.openclaw/memory');
206
+ const files = new Map<string, FileEntry>();
207
+
208
+ if (!fs.existsSync(memoryDir)) {
209
+ return files;
210
+ }
211
+
212
+ const walk = (dir: string, prefix: string = ''): void => {
213
+ const entries = fs.readdirSync(dir);
214
+ for (const entry of entries) {
215
+ const fullPath = path.join(dir, entry);
216
+ const relativePath = path.join(prefix, entry).replace(/\\/g, '/');
217
+
218
+ if (fs.statSync(fullPath).isDirectory()) {
219
+ walk(fullPath, relativePath);
220
+ } else {
221
+ const stat = fs.statSync(fullPath);
222
+ const content = fs.readFileSync(fullPath);
223
+ const hash = this.storage.hash(content);
224
+
225
+ files.set(relativePath, {
226
+ path: relativePath,
227
+ hash,
228
+ size: stat.size,
229
+ mode: 'file',
230
+ lastModified: stat.mtime.getTime(),
231
+ });
232
+ }
233
+ }
234
+ };
235
+
236
+ walk(memoryDir);
237
+ return files;
238
+ }
239
+
240
+ /**
241
+ * Find untracked files
242
+ */
243
+ private findUntracked(committedTree: TreeObject, workingTree: Map<string, FileEntry>): string[] {
244
+ const untracked: string[] = [];
245
+ workingTree.forEach((entry, path) => {
246
+ if (!committedTree.files.has(path)) {
247
+ untracked.push(path);
248
+ }
249
+ });
250
+ return untracked;
251
+ }
252
+
253
+ /**
254
+ * Serialize tree for hashing
255
+ */
256
+ private serializeTree(tree: TreeObject): Record<string, unknown> {
257
+ const files: Record<string, unknown> = {};
258
+ tree.files.forEach((entry, path) => {
259
+ files[path] = {
260
+ hash: entry.hash,
261
+ size: entry.size,
262
+ mode: entry.mode,
263
+ };
264
+ });
265
+ return { files };
266
+ }
267
+ }