@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,324 @@
1
+ /**
2
+ * Stage Area for Trace
3
+ * Smart partial commits (like git add, but agent-native)
4
+ */
5
+
6
+ export interface StagedChange {
7
+ path: string;
8
+ type: 'add' | 'modify' | 'delete';
9
+ oldContent?: string;
10
+ newContent?: string;
11
+ timestamp: number;
12
+ }
13
+
14
+ export interface CommitProposal {
15
+ stagedChanges: StagedChange[];
16
+ message: string;
17
+ type: 'feature' | 'fix' | 'docs' | 'test' | 'refactor' | 'chore';
18
+ timestamp: number;
19
+ }
20
+
21
+ export class StageArea {
22
+ private staged: Map<string, StagedChange> = new Map();
23
+ private workingDirectory: Map<string, string> = new Map();
24
+ private lastCommit: Map<string, string> = new Map();
25
+
26
+ /**
27
+ * Stage a change
28
+ */
29
+ stageChange(
30
+ path: string,
31
+ type: 'add' | 'modify' | 'delete',
32
+ oldContent?: string,
33
+ newContent?: string
34
+ ): StagedChange {
35
+ const change: StagedChange = {
36
+ path,
37
+ type,
38
+ oldContent,
39
+ newContent,
40
+ timestamp: Date.now(),
41
+ };
42
+
43
+ this.staged.set(path, change);
44
+ return change;
45
+ }
46
+
47
+ /**
48
+ * Stage multiple changes at once
49
+ */
50
+ stageMultiple(changes: StagedChange[]): void {
51
+ for (const change of changes) {
52
+ this.staged.set(change.path, change);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Auto-stage all changes (git add .)
58
+ */
59
+ stageAll(workingDir: Map<string, string>): StagedChange[] {
60
+ const changes: StagedChange[] = [];
61
+
62
+ // Find modified/added files
63
+ for (const [path, content] of workingDir) {
64
+ const oldContent = this.lastCommit.get(path);
65
+ if (oldContent === undefined) {
66
+ // New file
67
+ changes.push({
68
+ path,
69
+ type: 'add',
70
+ newContent: content,
71
+ timestamp: Date.now(),
72
+ });
73
+ } else if (oldContent !== content) {
74
+ // Modified file
75
+ changes.push({
76
+ path,
77
+ type: 'modify',
78
+ oldContent,
79
+ newContent: content,
80
+ timestamp: Date.now(),
81
+ });
82
+ }
83
+ }
84
+
85
+ // Find deleted files
86
+ for (const [path] of this.lastCommit) {
87
+ if (!workingDir.has(path)) {
88
+ changes.push({
89
+ path,
90
+ type: 'delete',
91
+ oldContent: this.lastCommit.get(path),
92
+ timestamp: Date.now(),
93
+ });
94
+ }
95
+ }
96
+
97
+ this.stageMultiple(changes);
98
+ return changes;
99
+ }
100
+
101
+ /**
102
+ * Unstage a file
103
+ */
104
+ unstageFile(path: string): boolean {
105
+ return this.staged.delete(path);
106
+ }
107
+
108
+ /**
109
+ * Unstage all
110
+ */
111
+ unstageAll(): void {
112
+ this.staged.clear();
113
+ }
114
+
115
+ /**
116
+ * Get staged changes
117
+ */
118
+ getStagedChanges(): StagedChange[] {
119
+ return Array.from(this.staged.values());
120
+ }
121
+
122
+ /**
123
+ * Check if file is staged
124
+ */
125
+ isStaged(path: string): boolean {
126
+ return this.staged.has(path);
127
+ }
128
+
129
+ /**
130
+ * Get staging status
131
+ */
132
+ getStatus(): {
133
+ staged: number;
134
+ unstaged: number;
135
+ untracked: number;
136
+ } {
137
+ let unstaged = 0;
138
+ let untracked = 0;
139
+
140
+ for (const [path, content] of this.workingDirectory) {
141
+ if (this.staged.has(path)) continue; // Already staged
142
+
143
+ const lastContent = this.lastCommit.get(path);
144
+ if (lastContent === undefined) {
145
+ untracked++;
146
+ } else if (lastContent !== content) {
147
+ unstaged++;
148
+ }
149
+ }
150
+
151
+ return {
152
+ staged: this.staged.size,
153
+ unstaged,
154
+ untracked,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Create commit from staged changes
160
+ */
161
+ createCommit(message: string, type: 'feature' | 'fix' | 'docs' | 'test' | 'refactor' | 'chore' = 'chore'): CommitProposal {
162
+ if (this.staged.size === 0) {
163
+ throw new Error('No staged changes to commit');
164
+ }
165
+
166
+ return {
167
+ stagedChanges: Array.from(this.staged.values()),
168
+ message,
169
+ type,
170
+ timestamp: Date.now(),
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Smart stage by semantic type
176
+ * Stage only 'feat:' changes for feature commit, etc.
177
+ */
178
+ stageByType(commitType: 'feature' | 'fix' | 'docs' | 'test' | 'refactor' | 'chore'): StagedChange[] {
179
+ const patterns: Record<string, RegExp[]> = {
180
+ feature: [/^(src|lib)\/.+\.(ts|js)$/, /feat:/i],
181
+ fix: [/^(src|lib)\/.+\.(ts|js)$/, /fix:/i],
182
+ docs: [/\.md$/, /^docs\//],
183
+ test: [/\.test\.(ts|js)$/, /test:/i],
184
+ refactor: [/^(src|lib)\/.+\.(ts|js)$/],
185
+ chore: [/^(\.|\w+\.json|\.yml)/, /chore:/i],
186
+ };
187
+
188
+ const relevantPatterns = patterns[commitType] || [];
189
+ const staged: StagedChange[] = [];
190
+
191
+ for (const change of this.staged.values()) {
192
+ if (relevantPatterns.some(p => p.test(change.path))) {
193
+ staged.push(change);
194
+ }
195
+ }
196
+
197
+ return staged;
198
+ }
199
+
200
+ /**
201
+ * Discard staged changes (reset before commit)
202
+ */
203
+ discardStaged(): void {
204
+ this.staged.clear();
205
+ }
206
+
207
+ /**
208
+ * Update working directory reference
209
+ */
210
+ updateWorkingDirectory(files: Map<string, string>): void {
211
+ this.workingDirectory = new Map(files);
212
+ }
213
+
214
+ /**
215
+ * Record commit (update lastCommit reference)
216
+ */
217
+ recordCommit(files: Map<string, string>): void {
218
+ this.lastCommit = new Map(files);
219
+ this.staged.clear();
220
+ }
221
+
222
+ /**
223
+ * Diff staged vs last commit
224
+ */
225
+ getDiff(): Map<string, { old?: string; new?: string }> {
226
+ const diff = new Map<string, { old?: string; new?: string }>();
227
+
228
+ for (const change of this.staged.values()) {
229
+ diff.set(change.path, {
230
+ old: change.oldContent,
231
+ new: change.newContent,
232
+ });
233
+ }
234
+
235
+ return diff;
236
+ }
237
+
238
+ /**
239
+ * Get staging stats
240
+ */
241
+ getStats(): {
242
+ stagedFiles: number;
243
+ addedFiles: number;
244
+ modifiedFiles: number;
245
+ deletedFiles: number;
246
+ estimatedSize: number;
247
+ } {
248
+ const changes = Array.from(this.staged.values());
249
+ let estimatedSize = 0;
250
+
251
+ const addedFiles = changes.filter(c => c.type === 'add').length;
252
+ const modifiedFiles = changes.filter(c => c.type === 'modify').length;
253
+ const deletedFiles = changes.filter(c => c.type === 'delete').length;
254
+
255
+ for (const change of changes) {
256
+ if (change.newContent) {
257
+ estimatedSize += change.newContent.length;
258
+ }
259
+ }
260
+
261
+ return {
262
+ stagedFiles: changes.length,
263
+ addedFiles,
264
+ modifiedFiles,
265
+ deletedFiles,
266
+ estimatedSize,
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Detect type from staged changes
272
+ */
273
+ detectCommitType(): 'feature' | 'fix' | 'docs' | 'test' | 'refactor' | 'chore' {
274
+ const changes = Array.from(this.staged.values());
275
+
276
+ // Check file patterns
277
+ const testFiles = changes.filter(c => /\.test\.(ts|js)$/.test(c.path)).length;
278
+ const docFiles = changes.filter(c => /\.md$/.test(c.path)).length;
279
+ const srcFiles = changes.filter(c => /^(src|lib)\/.+\.(ts|js)$/.test(c.path)).length;
280
+
281
+ if (testFiles > srcFiles) return 'test';
282
+ if (docFiles > 0 && srcFiles === 0) return 'docs';
283
+ if (srcFiles > 0) return 'feature'; // Default to feature for code changes
284
+
285
+ return 'chore';
286
+ }
287
+
288
+ /**
289
+ * Validate commit (all files present, no conflicts)
290
+ */
291
+ validateCommit(): { valid: boolean; errors: string[] } {
292
+ const errors: string[] = [];
293
+
294
+ if (this.staged.size === 0) {
295
+ errors.push('No staged changes');
296
+ return { valid: false, errors };
297
+ }
298
+
299
+ // Check for conflicting changes (e.g., add + delete same file)
300
+ const pathCounts = new Map<string, number>();
301
+ for (const change of this.staged.values()) {
302
+ pathCounts.set(change.path, (pathCounts.get(change.path) || 0) + 1);
303
+ }
304
+
305
+ for (const [path, count] of pathCounts) {
306
+ if (count > 1) {
307
+ errors.push(`File ${path} has multiple staged changes`);
308
+ }
309
+ }
310
+
311
+ return { valid: errors.length === 0, errors };
312
+ }
313
+
314
+ /**
315
+ * Clear stage (for testing)
316
+ */
317
+ clear(): void {
318
+ this.staged.clear();
319
+ this.workingDirectory.clear();
320
+ this.lastCommit.clear();
321
+ }
322
+ }
323
+
324
+ export default StageArea;
package/src/stash.ts ADDED
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Stash for Trace
3
+ * Temporary work storage (save WIP without committing)
4
+ */
5
+
6
+ export interface StashedWork {
7
+ id: string;
8
+ description: string;
9
+ timestamp: number;
10
+ files: Map<string, string>;
11
+ branch: string;
12
+ author: string;
13
+ }
14
+
15
+ export interface StashEntry {
16
+ id: string;
17
+ description: string;
18
+ timestamp: number;
19
+ branch: string;
20
+ author: string;
21
+ fileCount: number;
22
+ }
23
+
24
+ export class Stash {
25
+ private stashes: Map<string, StashedWork> = new Map();
26
+ private stashStack: string[] = []; // LIFO stack for pop
27
+ private authorStashes: Map<string, string[]> = new Map(); // author -> [stash IDs]
28
+
29
+ /**
30
+ * Stash current work
31
+ */
32
+ stash(
33
+ files: Map<string, string>,
34
+ description: string,
35
+ branch: string,
36
+ author: string
37
+ ): StashedWork {
38
+ const id = this.generateStashId();
39
+ const stashed: StashedWork = {
40
+ id,
41
+ description,
42
+ timestamp: Date.now(),
43
+ files: new Map(files),
44
+ branch,
45
+ author,
46
+ };
47
+
48
+ this.stashes.set(id, stashed);
49
+ this.stashStack.push(id);
50
+
51
+ if (!this.authorStashes.has(author)) {
52
+ this.authorStashes.set(author, []);
53
+ }
54
+ this.authorStashes.get(author)!.push(id);
55
+
56
+ return stashed;
57
+ }
58
+
59
+ /**
60
+ * Pop stash (apply and remove most recent)
61
+ */
62
+ pop(): StashedWork | undefined {
63
+ if (this.stashStack.length === 0) {
64
+ return undefined;
65
+ }
66
+
67
+ const id = this.stashStack.pop()!;
68
+ const stashed = this.stashes.get(id);
69
+
70
+ if (stashed) {
71
+ // Remove from author tracking
72
+ const authorStashes = this.authorStashes.get(stashed.author) || [];
73
+ this.authorStashes.set(
74
+ stashed.author,
75
+ authorStashes.filter(s => s !== id)
76
+ );
77
+
78
+ this.stashes.delete(id);
79
+ }
80
+
81
+ return stashed;
82
+ }
83
+
84
+ /**
85
+ * Apply stash without removing (like git stash apply)
86
+ */
87
+ apply(stashId: string): StashedWork | undefined {
88
+ return this.stashes.get(stashId);
89
+ }
90
+
91
+ /**
92
+ * List all stashes
93
+ */
94
+ list(): StashEntry[] {
95
+ return Array.from(this.stashes.values()).map(s => ({
96
+ id: s.id,
97
+ description: s.description,
98
+ timestamp: s.timestamp,
99
+ branch: s.branch,
100
+ author: s.author,
101
+ fileCount: s.files.size,
102
+ }));
103
+ }
104
+
105
+ /**
106
+ * Get stashes for author
107
+ */
108
+ getByAuthor(author: string): StashEntry[] {
109
+ const ids = this.authorStashes.get(author) || [];
110
+ return ids
111
+ .map(id => this.stashes.get(id))
112
+ .filter((s): s is StashedWork => s !== undefined)
113
+ .map(s => ({
114
+ id: s.id,
115
+ description: s.description,
116
+ timestamp: s.timestamp,
117
+ branch: s.branch,
118
+ author: s.author,
119
+ fileCount: s.files.size,
120
+ }));
121
+ }
122
+
123
+ /**
124
+ * Get stashes for branch
125
+ */
126
+ getByBranch(branch: string): StashEntry[] {
127
+ return Array.from(this.stashes.values())
128
+ .filter(s => s.branch === branch)
129
+ .map(s => ({
130
+ id: s.id,
131
+ description: s.description,
132
+ timestamp: s.timestamp,
133
+ branch: s.branch,
134
+ author: s.author,
135
+ fileCount: s.files.size,
136
+ }));
137
+ }
138
+
139
+ /**
140
+ * Drop stash (delete without applying)
141
+ */
142
+ drop(stashId: string): boolean {
143
+ const stashed = this.stashes.get(stashId);
144
+ if (!stashed) {
145
+ return false;
146
+ }
147
+
148
+ this.stashes.delete(stashId);
149
+ this.stashStack = this.stashStack.filter(id => id !== stashId);
150
+
151
+ const authorStashes = this.authorStashes.get(stashed.author) || [];
152
+ this.authorStashes.set(
153
+ stashed.author,
154
+ authorStashes.filter(s => s !== stashId)
155
+ );
156
+
157
+ return true;
158
+ }
159
+
160
+ /**
161
+ * Clear all stashes
162
+ */
163
+ clearAll(): void {
164
+ this.stashes.clear();
165
+ this.stashStack = [];
166
+ this.authorStashes.clear();
167
+ }
168
+
169
+ /**
170
+ * Get stash details
171
+ */
172
+ getStash(stashId: string): StashedWork | undefined {
173
+ return this.stashes.get(stashId);
174
+ }
175
+
176
+ /**
177
+ * Check if stash exists
178
+ */
179
+ hasStash(stashId: string): boolean {
180
+ return this.stashes.has(stashId);
181
+ }
182
+
183
+ /**
184
+ * Get statistics
185
+ */
186
+ getStats(): {
187
+ totalStashes: number;
188
+ oldestStash?: StashEntry;
189
+ newestStash?: StashEntry;
190
+ totalFiles: number;
191
+ } {
192
+ const entries = this.list();
193
+ if (entries.length === 0) {
194
+ return { totalStashes: 0, totalFiles: 0 };
195
+ }
196
+
197
+ const oldest = entries.reduce((a, b) => (a.timestamp < b.timestamp ? a : b));
198
+ const newest = entries.reduce((a, b) => (a.timestamp > b.timestamp ? a : b));
199
+
200
+ const totalFiles = Array.from(this.stashes.values()).reduce(
201
+ (sum, s) => sum + s.files.size,
202
+ 0
203
+ );
204
+
205
+ return {
206
+ totalStashes: entries.length,
207
+ oldestStash: oldest,
208
+ newestStash: newest,
209
+ totalFiles,
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Auto-stash before checkout (safety)
215
+ */
216
+ autoStashIfDirty(
217
+ files: Map<string, string>,
218
+ lastCommit: Map<string, string>,
219
+ branch: string,
220
+ author: string
221
+ ): StashedWork | null {
222
+ // Check if working directory is dirty
223
+ let isDirty = false;
224
+
225
+ for (const [path, content] of files) {
226
+ if (lastCommit.get(path) !== content) {
227
+ isDirty = true;
228
+ break;
229
+ }
230
+ }
231
+
232
+ // Check for new files
233
+ if (!isDirty) {
234
+ for (const path of files.keys()) {
235
+ if (!lastCommit.has(path)) {
236
+ isDirty = true;
237
+ break;
238
+ }
239
+ }
240
+ }
241
+
242
+ if (isDirty) {
243
+ return this.stash(files, `Auto-stash before checkout (${branch})`, branch, author);
244
+ }
245
+
246
+ return null;
247
+ }
248
+
249
+ /**
250
+ * Format stash list for display
251
+ */
252
+ static formatList(entries: StashEntry[]): string {
253
+ if (entries.length === 0) {
254
+ return 'No stashes found.';
255
+ }
256
+
257
+ let output = `\n📦 STASHES (${entries.length} total):\n`;
258
+
259
+ for (let i = 0; i < entries.length; i++) {
260
+ const entry = entries[i];
261
+ output += `${i}. stash@{${i}} (${entry.id.substring(0, 7)})\n`;
262
+ output += ` Description: ${entry.description}\n`;
263
+ output += ` Author: ${entry.author} | Branch: ${entry.branch}\n`;
264
+ output += ` Files: ${entry.fileCount} | Created: ${new Date(entry.timestamp).toISOString()}\n`;
265
+ }
266
+
267
+ return output;
268
+ }
269
+
270
+ /**
271
+ * Generate stash ID
272
+ */
273
+ private generateStashId(): string {
274
+ return `stash-${Date.now()}-${Math.random().toString(36).substring(7)}`;
275
+ }
276
+ }
277
+
278
+ export default Stash;
package/src/storage.ts ADDED
@@ -0,0 +1,131 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as crypto from 'crypto';
4
+ import { CommitObject, TreeObject, FileEntry } from './types';
5
+
6
+ export class Storage {
7
+ private basePath: string;
8
+ private objectsDir: string;
9
+ private refsDir: string;
10
+
11
+ constructor(basePath: string = path.join(process.env.HOME || '', '.openclaw/memory-git')) {
12
+ this.basePath = basePath;
13
+ this.objectsDir = path.join(basePath, 'objects');
14
+ this.refsDir = path.join(basePath, 'refs');
15
+ this.ensureDirectories();
16
+ }
17
+
18
+ private ensureDirectories(): void {
19
+ [this.basePath, this.objectsDir, this.refsDir].forEach(dir => {
20
+ if (!fs.existsSync(dir)) {
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ }
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Compute SHA-256 hash of content
28
+ */
29
+ hash(content: string | Buffer): string {
30
+ const buffer = typeof content === 'string' ? Buffer.from(content, 'utf-8') : content;
31
+ return crypto.createHash('sha256').update(buffer).digest('hex');
32
+ }
33
+
34
+ /**
35
+ * Save commit object to storage
36
+ */
37
+ saveCommit(commit: CommitObject): void {
38
+ const commitPath = path.join(this.objectsDir, `${commit.hash}.json`);
39
+ fs.writeFileSync(commitPath, JSON.stringify(commit, this.replaceMapValues, 2));
40
+ }
41
+
42
+ /**
43
+ * Load commit object from storage
44
+ */
45
+ loadCommit(hash: string): CommitObject | null {
46
+ const commitPath = path.join(this.objectsDir, `${hash}.json`);
47
+ if (!fs.existsSync(commitPath)) {
48
+ return null;
49
+ }
50
+ const content = fs.readFileSync(commitPath, 'utf-8');
51
+ const commit = JSON.parse(content) as CommitObject;
52
+ const filesRecord = commit.tree.files as unknown as Record<string, FileEntry>;
53
+ commit.tree.files = new Map(Object.entries(filesRecord));
54
+ return commit;
55
+ }
56
+
57
+ /**
58
+ * Save reference (branch/tag)
59
+ */
60
+ saveRef(name: string, hash: string): void {
61
+ const refPath = path.join(this.refsDir, name);
62
+ fs.mkdirSync(path.dirname(refPath), { recursive: true });
63
+ fs.writeFileSync(refPath, hash);
64
+ }
65
+
66
+ /**
67
+ * Load reference
68
+ */
69
+ loadRef(name: string): string | null {
70
+ const refPath = path.join(this.refsDir, name);
71
+ if (!fs.existsSync(refPath)) {
72
+ return null;
73
+ }
74
+ return fs.readFileSync(refPath, 'utf-8').trim();
75
+ }
76
+
77
+ /**
78
+ * List all commits
79
+ */
80
+ listCommits(): string[] {
81
+ if (!fs.existsSync(this.objectsDir)) {
82
+ return [];
83
+ }
84
+ return fs
85
+ .readdirSync(this.objectsDir)
86
+ .filter(f => f.endsWith('.json'))
87
+ .map(f => f.replace('.json', ''));
88
+ }
89
+
90
+ /**
91
+ * Delete commit
92
+ */
93
+ deleteCommit(hash: string): void {
94
+ const commitPath = path.join(this.objectsDir, `${hash}.json`);
95
+ if (fs.existsSync(commitPath)) {
96
+ fs.unlinkSync(commitPath);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get commit history chain
102
+ */
103
+ getHistory(startHash: string): CommitObject[] {
104
+ const history: CommitObject[] = [];
105
+ let current: CommitObject | null = this.loadCommit(startHash);
106
+
107
+ while (current !== null) {
108
+ history.push(current);
109
+ if (current.parent === null) {
110
+ break;
111
+ }
112
+ current = this.loadCommit(current.parent);
113
+ }
114
+
115
+ return history;
116
+ }
117
+
118
+ /**
119
+ * Helper to serialize Maps as objects for JSON
120
+ */
121
+ private replaceMapValues(key: string, value: unknown): unknown {
122
+ if (value instanceof Map) {
123
+ const obj: Record<string, unknown> = {};
124
+ value.forEach((v, k) => {
125
+ obj[k] = v;
126
+ });
127
+ return obj;
128
+ }
129
+ return value;
130
+ }
131
+ }