@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.
- package/.gitignore +115 -0
- package/.trace/progress.json +22 -0
- package/README.md +466 -0
- package/RELEASE-NOTES-1.5.0.md +410 -0
- package/STATUS.md +245 -0
- package/dist/auto-commit.d.ts +66 -0
- package/dist/auto-commit.d.ts.map +1 -0
- package/dist/auto-commit.js +180 -0
- package/dist/auto-commit.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +246 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +46 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +256 -0
- package/dist/commands.js.map +1 -0
- package/dist/diff.d.ts +23 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +106 -0
- package/dist/diff.js.map +1 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js.map +1 -0
- package/dist/index-cache.d.ts +35 -0
- package/dist/index-cache.d.ts.map +1 -0
- package/dist/index-cache.js +114 -0
- package/dist/index-cache.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/storage.d.ts +45 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +151 -0
- package/dist/storage.js.map +1 -0
- package/dist/sync.d.ts +60 -0
- package/dist/sync.js +184 -0
- package/dist/tags.d.ts +85 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +219 -0
- package/dist/tags.js.map +1 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/docs/.nojekyll +0 -0
- package/docs/README.md +73 -0
- package/docs/_config.yml +2 -0
- package/docs/index.html +960 -0
- package/docs-website/package.json +20 -0
- package/jest.config.js +21 -0
- package/package.json +50 -0
- package/scripts/init.ts +290 -0
- package/src/agent-audit.ts +270 -0
- package/src/agent-checkout.ts +227 -0
- package/src/agent-coordination.ts +318 -0
- package/src/async-queue.ts +203 -0
- package/src/auto-branching.ts +279 -0
- package/src/auto-commit.ts +166 -0
- package/src/cherry-pick.ts +252 -0
- package/src/chunked-upload.ts +224 -0
- package/src/cli-v2.ts +335 -0
- package/src/cli.ts +318 -0
- package/src/cliff-detection.ts +232 -0
- package/src/commands.ts +267 -0
- package/src/commit-hash-system.ts +351 -0
- package/src/compression.ts +176 -0
- package/src/conflict-resolution-ui.ts +277 -0
- package/src/conflict-visualization.ts +238 -0
- package/src/diff-formatter.ts +184 -0
- package/src/diff.ts +124 -0
- package/src/distributed-coordination.ts +273 -0
- package/src/git-interop.ts +316 -0
- package/src/index-cache.ts +88 -0
- package/src/index.ts +38 -0
- package/src/merge-engine.ts +143 -0
- package/src/message-search.ts +370 -0
- package/src/performance-monitoring.ts +236 -0
- package/src/rebase.ts +327 -0
- package/src/rollback.ts +215 -0
- package/src/semantic-grouping.ts +245 -0
- package/src/stage-area.ts +324 -0
- package/src/stash.ts +278 -0
- package/src/storage.ts +131 -0
- package/src/sync.ts +205 -0
- package/src/tags.ts +244 -0
- package/src/types.ts +119 -0
- package/src/webhooks.ts +119 -0
- package/src/workspace-isolation.ts +298 -0
- package/tests/auto-commit.test.ts +308 -0
- package/tests/checkout.test.ts +136 -0
- package/tests/commit.test.ts +118 -0
- package/tests/diff.test.ts +191 -0
- package/tests/github.test.ts +94 -0
- package/tests/integration.test.ts +267 -0
- package/tests/log.test.ts +125 -0
- package/tests/phase2-integration.test.ts +370 -0
- package/tests/storage.test.ts +167 -0
- package/tests/tags.test.ts +477 -0
- package/tests/types.test.ts +75 -0
- package/tests/v1.1/agent-audit.test.ts +472 -0
- package/tests/v1.1/agent-coordination.test.ts +308 -0
- package/tests/v1.1/async-queue.test.ts +253 -0
- package/tests/v1.1/comprehensive.test.ts +521 -0
- package/tests/v1.1/diff-formatter.test.ts +238 -0
- package/tests/v1.1/integration.test.ts +389 -0
- package/tests/v1.1/onboarding.test.ts +365 -0
- package/tests/v1.1/rollback.test.ts +370 -0
- package/tests/v1.1/semantic-grouping.test.ts +230 -0
- package/tests/v1.2/chunked-upload.test.ts +301 -0
- package/tests/v1.2/cliff-detection.test.ts +272 -0
- package/tests/v1.2/commit-hash-system.test.ts +288 -0
- package/tests/v1.2/compression.test.ts +220 -0
- package/tests/v1.2/conflict-visualization.test.ts +263 -0
- package/tests/v1.2/distributed.test.ts +261 -0
- package/tests/v1.2/performance-monitoring.test.ts +328 -0
- package/tests/v1.3/auto-branching.test.ts +270 -0
- package/tests/v1.3/message-search.test.ts +264 -0
- package/tests/v1.3/stage-area.test.ts +330 -0
- package/tests/v1.3/stash-rebase-cherry-pick.test.ts +361 -0
- package/tests/v1.4/cli.test.ts +171 -0
- package/tests/v1.4/conflict-resolution-advanced.test.ts +429 -0
- package/tests/v1.4/conflict-resolution-ui.test.ts +286 -0
- package/tests/v1.4/workspace-isolation-advanced.test.ts +382 -0
- package/tests/v1.4/workspace-isolation.test.ts +268 -0
- package/tests/v1.5/agent-coordination.real.test.ts +401 -0
- package/tests/v1.5/cli-v2.test.ts +354 -0
- package/tests/v1.5/git-interop.real.test.ts +358 -0
- package/tests/v1.5/integration-testing.real.test.ts +440 -0
- 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;
|
package/src/commands.ts
ADDED
|
@@ -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
|
+
}
|