* Stop recording and save the trace.
()
| 101 | * Stop recording and save the trace. |
| 102 | */ |
| 103 | stop() { |
| 104 | if (!this.recording || !this.current) return null; |
| 105 | this.current.endedAt = new Date().toISOString(); |
| 106 | this.current.durationMs = Date.now() - new Date(this.current.startedAt).getTime(); |
| 107 | // Redact prompt — it can contain pasted secrets, file paths, or |
| 108 | // proprietary data the user wouldn't want shared via /share. |
| 109 | this.current.prompt = redactString(this.current.prompt || ''); |
| 110 | this.recording = false; |
| 111 | |
| 112 | // Save to disk |
| 113 | if (!fs.existsSync(this.tracesDir)) fs.mkdirSync(this.tracesDir, { recursive: true, mode: DIR_MODE }); |
| 114 | // Validate trace ID — defends against accidental injection via stop() |
| 115 | // being called with a tampered current object. |
| 116 | const id = String(this.current.id || '').replace(/[^A-Za-z0-9_-]/g, ''); |
| 117 | if (!id) { this.current = null; return null; } |
| 118 | const filePath = path.join(this.tracesDir, `${id}.json`); |
| 119 | if (!filePath.startsWith(this.tracesDir + path.sep)) { this.current = null; return null; } |
| 120 | const tmpPath = filePath + `.tmp.${process.pid}.${Date.now()}`; |
| 121 | fs.writeFileSync(tmpPath, JSON.stringify(this.current, null, 2), { mode: FILE_MODE }); |
| 122 | fs.renameSync(tmpPath, filePath); |
| 123 | try { fs.chmodSync(filePath, FILE_MODE); } catch {} |
| 124 | |
| 125 | const saved = this.current; |
| 126 | this.current = null; |
| 127 | return saved; |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * List all saved traces. |
no test coverage detected