Skip to content

Instantly share code, notes, and snippets.

@le-dawg
Last active July 19, 2026 21:51
Show Gist options
  • Select an option

  • Save le-dawg/1034697c11e6c4b0453b614867d921c9 to your computer and use it in GitHub Desktop.

Select an option

Save le-dawg/1034697c11e6c4b0453b614867d921c9 to your computer and use it in GitHub Desktop.
Apple Terminal Global Log for macOS: a friendly, no-wrapper way to keep one capped JSONL history so you can analyze past work or backtrack on one of those bad days. If people want it, an Apple user-auth-backed encryption layer can be added next.

Apple Terminal Global Log for macOS

Friendly, passive Apple Terminal history harvesting for macOS.

It writes the maximum recoverable content from Apple Terminal into one capped JSONL file at ~/.terminal_global_history.jsonl without changing Terminal's shell launch chain.

This is meant for people who sometimes need to analyze past work, retrace steps, or backtrack on one of those bad days when you really need to understand what happened.

If people are interested, an optional encryption layer tied to Apple user-level auth can be added later so the log stays more protected without changing the overall workflow.

What It Captures

  • Live scrollback from open Apple Terminal tabs via AppleScript.
  • Durable Apple-managed session artifacts from ~/.zsh_sessions.

What It Does Not Do

  • It does not wrap your shell.
  • It does not disable Apple Terminal session restore.
  • It does not disable Apple per-session history.
  • It does not guarantee a mathematically perfect full transcript.

Install

  1. Download this Gist or clone it.
  2. From the folder containing these files, run:
zsh install.sh
  1. When prompted, check Terminal > Settings > Profiles > Red Sands 1 > Window. If scrollback is already set to memory available, keep it. Otherwise, use the highest scrollback setting you are comfortable with for better live capture.

What Install Does

  • Backs up your current Terminal preferences to ~/.terminal-global-log/backups/com.apple.Terminal.before-install.plist
  • Compiles the Swift collector into ~/.terminal-global-log/bin/terminal-global-log
  • Copies the AppleScript helper and uninstall script into ~/.terminal-global-log/scripts
  • Creates ~/Library/LaunchAgents/com.thedawgctor.terminal-global-log.plist
  • Runs one initial collection pass
  • Loads a LaunchAgent that runs at login, every 30 minutes, and when ~/.zsh_sessions changes

Installed Paths

  • Canonical log: ~/.terminal_global_history.jsonl
  • Collector state: ~/.terminal-global-log/state/collector-state.json
  • LaunchAgent: ~/Library/LaunchAgents/com.thedawgctor.terminal-global-log.plist
  • Restore instructions: ~/.terminal-global-log/RESTORE.txt

Uninstall

zsh ~/.terminal-global-log/scripts/uninstall.sh

If you also want your prior Terminal preferences back, run the defaults import command printed by the uninstall script.

Test-Friendly Flags

  • TERMINAL_GLOBAL_LOG_SKIP_DIALOG=1 skips the reminder dialog.
  • TERMINAL_GLOBAL_LOG_SKIP_LAUNCHCTL=1 skips loading the LaunchAgent.
  • TERMINAL_GLOBAL_LOG_SOURCE_ROOT=/path/to/this/folder overrides the source folder used by install.sh.

Publish As Gist

If you have gh authenticated, you can publish this folder directly:

zsh publish-gist.sh --public

Or keep it unlisted:

zsh publish-gist.sh --secret
import Foundation
final class AppleTerminalSnapshotSource: HarvestSource {
private let config: CollectorConfig
private let fieldSeparator = Character(UnicodeScalar(0x1F)!)
private let recordSeparator = Character(UnicodeScalar(0x1E)!)
init(config: CollectorConfig) {
self.config = config
}
func harvest(state: inout CollectorState, now: Date) throws -> [CanonicalRecord] {
if let fixturePath = config.snapshotFixturePath {
let text = try String(contentsOf: fixturePath, encoding: .utf8)
return parseFixtureTSV(text: text, state: &state, now: now)
}
guard FileManager.default.fileExists(atPath: config.snapshotScriptPath.path) else {
return []
}
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
process.arguments = [config.snapshotScriptPath.path]
let stdout = Pipe()
process.standardOutput = stdout
do {
try process.run()
} catch {
return []
}
process.waitUntilExit()
guard process.terminationStatus == 0 else {
return []
}
let text = String(decoding: stdout.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
return parseAppleScriptDump(text: text, state: &state, now: now)
}
private func parseFixtureTSV(text: String, state: inout CollectorState, now: Date) -> [CanonicalRecord] {
var records: [CanonicalRecord] = []
let timestamp = ISO8601DateFormatter.terminalGlobalLog.string(from: now)
for rawLine in text.split(separator: "\n", omittingEmptySubsequences: true) {
let parts = String(rawLine).split(separator: "\t", maxSplits: 3, omittingEmptySubsequences: false).map(String.init)
guard parts.count == 4 else {
continue
}
appendRecord(parts: parts, sourcePathPrefix: "fixture", timestamp: timestamp, state: &state, records: &records)
}
return records
}
private func parseAppleScriptDump(text: String, state: inout CollectorState, now: Date) -> [CanonicalRecord] {
var records: [CanonicalRecord] = []
let timestamp = ISO8601DateFormatter.terminalGlobalLog.string(from: now)
for rawRecord in text.split(separator: recordSeparator, omittingEmptySubsequences: true) {
let parts = String(rawRecord).split(separator: fieldSeparator, maxSplits: 3, omittingEmptySubsequences: false).map(String.init)
guard parts.count == 4 else {
continue
}
appendRecord(parts: parts, sourcePathPrefix: "terminal", timestamp: timestamp, state: &state, records: &records)
}
return records
}
private func appendRecord(
parts: [String],
sourcePathPrefix: String,
timestamp: String,
state: inout CollectorState,
records: inout [CanonicalRecord]
) {
let windowID = Int(parts[0]) ?? 0
let tabID = Int(parts[1]) ?? 0
let tty = parts[2]
let payload = parts[3]
let sessionID = "terminal/\(windowID)/\(tabID)/\(tty)"
let payloadHash = sha256Hex(payload)
if state.terminalSnapshots[sessionID]?.payloadHash == payloadHash {
return
}
let sourcePath = "\(sourcePathPrefix):\(sessionID)"
let payloadSize = UInt64(Data(payload.utf8).count)
let record = CanonicalRecord(
schemaVersion: 1,
recordID: makeRecordID(
sourceKind: "terminal_snapshot",
sourcePath: sourcePath,
generation: 1,
start: 0,
end: payloadSize,
payload: payload
),
recordType: "terminal_snapshot_chunk",
sessionID: sessionID,
sourceKind: "terminal_snapshot",
sourcePath: sourcePath,
sourceGeneration: 1,
sourceOffsetStart: 0,
sourceOffsetEnd: payloadSize,
capturedAt: timestamp,
sessionWindowID: windowID,
sessionTabID: tabID,
tty: tty.isEmpty ? nil : tty,
hostname: Host.current().localizedName ?? "unknown-host",
cwd: nil,
payload: payload
)
records.append(record)
state.terminalSnapshots[sessionID] = SnapshotCursor(payloadHash: payloadHash, lastCapturedAt: timestamp)
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.thedawgctor.terminal-global-log</string>
<key>ProgramArguments</key>
<array>
<string>__HOME__/.terminal-global-log/bin/terminal-global-log</string>
<string>collect</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>1800</integer>
<key>WatchPaths</key>
<array>
<string>__HOME__/.zsh_sessions</string>
</array>
<key>StandardOutPath</key>
<string>__HOME__/.terminal-global-log/logs/collector.stdout.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/.terminal-global-log/logs/collector.stderr.log</string>
</dict>
</plist>
set fieldSep to ASCII character 31
set recordSep to ASCII character 30
set outText to ""
tell application "Terminal"
repeat with win in windows
set winId to (id of win) as text
repeat with idx from 1 to (count of tabs of win)
tell tab idx of win
try
set ttyValue to (tty as text)
on error
set ttyValue to ""
end try
try
set payloadText to (contents as text)
on error
set payloadText to ""
end try
end tell
set outText to outText & winId & fieldSep & (idx as text) & fieldSep & ttyValue & fieldSep & payloadText & recordSep
end repeat
end repeat
end tell
return outText
import Foundation
final class GlobalLogStore {
private let canonicalLogPath: URL
private let byteLimit: UInt64
private let encoder = JSONEncoder()
init(canonicalLogPath: URL, byteLimit: UInt64) {
self.canonicalLogPath = canonicalLogPath
self.byteLimit = byteLimit
encoder.outputFormatting = [.sortedKeys]
}
func append(records: [CanonicalRecord]) throws {
try FileManager.default.createDirectory(
at: canonicalLogPath.deletingLastPathComponent(),
withIntermediateDirectories: true,
attributes: nil
)
try repairTrailingPartialLineIfNeeded()
guard !records.isEmpty else {
try compactIfNeeded()
return
}
if !FileManager.default.fileExists(atPath: canonicalLogPath.path) {
_ = FileManager.default.createFile(atPath: canonicalLogPath.path, contents: nil)
}
let handle = try FileHandle(forWritingTo: canonicalLogPath)
defer { try? handle.close() }
_ = try handle.seekToEnd()
for record in records {
let data = try encoder.encode(record)
try handle.write(contentsOf: data)
try handle.write(contentsOf: Data([0x0a]))
}
try compactIfNeeded()
}
func compactIfNeeded() throws {
guard FileManager.default.fileExists(atPath: canonicalLogPath.path) else {
return
}
let attributes = try FileManager.default.attributesOfItem(atPath: canonicalLogPath.path)
let currentSize = (attributes[.size] as? NSNumber)?.uint64Value ?? 0
guard currentSize > byteLimit else {
return
}
let data = try Data(contentsOf: canonicalLogPath)
let lines = data.split(separator: 0x0a, omittingEmptySubsequences: true)
struct RetainedLine {
let offset: UInt64
let length: UInt64
}
var offset: UInt64 = 0
var retained: [RetainedLine] = []
var retainedBytes: UInt64 = 0
for line in lines {
let lineLength = UInt64(line.count + 1)
retained.append(RetainedLine(offset: offset, length: lineLength))
retainedBytes += lineLength
while retainedBytes > byteLimit, !retained.isEmpty {
retainedBytes -= retained.removeFirst().length
}
offset += lineLength
}
guard let first = retained.first else {
return
}
let tempURL = canonicalLogPath.appendingPathExtension("tmp")
let sliceStart = Int(first.offset)
let retainedData = data.subdata(in: sliceStart..<data.count)
try retainedData.write(to: tempURL, options: .atomic)
if FileManager.default.fileExists(atPath: canonicalLogPath.path) {
try FileManager.default.removeItem(at: canonicalLogPath)
}
try FileManager.default.moveItem(at: tempURL, to: canonicalLogPath)
}
private func repairTrailingPartialLineIfNeeded() throws {
guard FileManager.default.fileExists(atPath: canonicalLogPath.path) else {
return
}
let data = try Data(contentsOf: canonicalLogPath)
guard let lastByte = data.last else {
return
}
guard lastByte != 0x0a else {
return
}
guard let newlineIndex = data.lastIndex(of: 0x0a) else {
try Data().write(to: canonicalLogPath, options: .atomic)
return
}
let repaired = data.prefix(upTo: data.index(after: newlineIndex))
try repaired.write(to: canonicalLogPath, options: .atomic)
}
}
#!/bin/zsh
set -euo pipefail
SOURCE_ROOT="${TERMINAL_GLOBAL_LOG_SOURCE_ROOT:-$(cd -- "$(dirname -- "$0")" && pwd)}"
APP_ROOT="$HOME/.terminal-global-log"
BIN_DIR="$APP_ROOT/bin"
APP_SCRIPT_DIR="$APP_ROOT/scripts"
LOG_DIR="$APP_ROOT/logs"
STATE_DIR="$APP_ROOT/state"
BACKUP_DIR="$APP_ROOT/backups"
LAUNCHD_SRC="$SOURCE_ROOT/com.thedawgctor.terminal-global-log.plist.template"
LAUNCHD_DST="$HOME/Library/LaunchAgents/com.thedawgctor.terminal-global-log.plist"
SCRIPT_SRC="$SOURCE_ROOT/dump_open_terminal_tabs.applescript"
UNINSTALL_SRC="$SOURCE_ROOT/uninstall.sh"
RESTORE_DST="$APP_ROOT/RESTORE.txt"
SCRIPT_DST="$APP_SCRIPT_DIR/dump_open_terminal_tabs.applescript"
UNINSTALL_DST="$APP_SCRIPT_DIR/uninstall.sh"
TERMINAL_BACKUP="$BACKUP_DIR/com.apple.Terminal.before-install.plist"
mkdir -p "$BIN_DIR" "$APP_SCRIPT_DIR" "$LOG_DIR" "$STATE_DIR" "$BACKUP_DIR" "$HOME/Library/LaunchAgents"
copy_if_needed() {
local src="$1"
local dst="$2"
if [[ "$src" == "$dst" ]]; then
return 0
fi
cp -p "$src" "$dst"
}
write_empty_terminal_backup() {
cat > "$TERMINAL_BACKUP" <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
EOF
}
write_restore_instructions() {
cat > "$RESTORE_DST" <<'EOF'
1. Unload the LaunchAgent:
launchctl bootout "gui/$(id -u)" "$HOME/Library/LaunchAgents/com.thedawgctor.terminal-global-log.plist" 2>/dev/null || true
2. Restore Terminal defaults from the backup created at install time:
defaults import com.apple.Terminal "$HOME/.terminal-global-log/backups/com.apple.Terminal.before-install.plist"
3. Remove the installed files:
rm -rf "$HOME/.terminal-global-log"
rm -f "$HOME/Library/LaunchAgents/com.thedawgctor.terminal-global-log.plist"
rm -f "$HOME/.terminal_global_history.jsonl"
4. Restart Terminal.
EOF
}
if ! defaults export com.apple.Terminal "$TERMINAL_BACKUP" >/dev/null 2>&1; then
write_empty_terminal_backup
fi
xcrun swiftc \
"$SOURCE_ROOT/Models.swift" \
"$SOURCE_ROOT/StateStore.swift" \
"$SOURCE_ROOT/AppleTerminalSnapshotSource.swift" \
"$SOURCE_ROOT/ZshSessionArtifactSource.swift" \
"$SOURCE_ROOT/GlobalLogStore.swift" \
"$SOURCE_ROOT/main.swift" \
-o "$BIN_DIR/terminal-global-log"
copy_if_needed "$SCRIPT_SRC" "$SCRIPT_DST"
copy_if_needed "$UNINSTALL_SRC" "$UNINSTALL_DST"
write_restore_instructions
chmod +x "$UNINSTALL_DST" "$BIN_DIR/terminal-global-log"
sed "s#__HOME__#$HOME#g" "$LAUNCHD_SRC" > "$LAUNCHD_DST"
"$BIN_DIR/terminal-global-log" collect || true
if [[ "${TERMINAL_GLOBAL_LOG_SKIP_LAUNCHCTL:-0}" != "1" ]]; then
launchctl bootout "gui/$(id -u)" "$LAUNCHD_DST" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$LAUNCHD_DST"
launchctl kickstart -k "gui/$(id -u)/com.thedawgctor.terminal-global-log"
fi
if [[ "${TERMINAL_GLOBAL_LOG_SKIP_DIALOG:-0}" != "1" ]]; then
osascript <<'APPLESCRIPT'
display dialog "Check Terminal > Settings > Profiles > Red Sands 1 > Window. If scrollback is already set to 'memory available', keep it. Otherwise, use the highest scrollback setting you are comfortable with for better live capture, then click Continue." buttons {"Continue"} default button "Continue"
APPLESCRIPT
fi
echo "Installed terminal-global-log to $APP_ROOT"
import Foundation
enum CollectorCommand: String {
case collect
case compact
case doctor
}
let config = CollectorConfig.load()
let stateStore = StateStore(statePath: config.statePath)
let logStore = GlobalLogStore(canonicalLogPath: config.canonicalLogPath, byteLimit: config.byteLimit)
let command = CollectorCommand(rawValue: CommandLine.arguments.dropFirst().first ?? "collect") ?? .collect
switch command {
case .collect:
var state = try stateStore.load()
let now = Date()
let snapshotSource = AppleTerminalSnapshotSource(config: config)
let artifactSource = ZshSessionArtifactSource(config: config)
var records = try snapshotSource.harvest(state: &state, now: now)
records += try artifactSource.harvest(state: &state, now: now)
try logStore.append(records: records)
try stateStore.save(state)
case .compact:
try logStore.compactIfNeeded()
case .doctor:
print("canonical_log=\(config.canonicalLogPath.path)")
print("state_path=\(config.statePath.path)")
print("zsh_sessions=\(config.zshSessionsDirectory.path)")
}
import CryptoKit
import Foundation
struct CollectorConfig {
let homeDirectory: URL
let appDirectory: URL
let canonicalLogPath: URL
let statePath: URL
let zshSessionsDirectory: URL
let snapshotFixturePath: URL?
let snapshotScriptPath: URL
let byteLimit: UInt64
static func load() -> CollectorConfig {
let environment = ProcessInfo.processInfo.environment
let homePath = environment["HOME"] ?? NSHomeDirectory()
let homeDirectory = URL(fileURLWithPath: homePath, isDirectory: true)
let appDirectory = homeDirectory.appendingPathComponent(".terminal-global-log", isDirectory: true)
let defaultLogPath = homeDirectory.appendingPathComponent(".terminal_global_history.jsonl").path
let defaultStatePath = appDirectory.appendingPathComponent("state/collector-state.json").path
let defaultSessionsPath = homeDirectory.appendingPathComponent(".zsh_sessions", isDirectory: true).path
let defaultScriptPath = appDirectory.appendingPathComponent("scripts/dump_open_terminal_tabs.applescript").path
let byteLimit = environment["TERMINAL_GLOBAL_LOG_BYTE_LIMIT"].flatMap(UInt64.init) ?? 1_073_741_824
return CollectorConfig(
homeDirectory: homeDirectory,
appDirectory: appDirectory,
canonicalLogPath: URL(fileURLWithPath: environment["TERMINAL_GLOBAL_LOG_FILE"] ?? defaultLogPath),
statePath: URL(fileURLWithPath: environment["TERMINAL_GLOBAL_LOG_STATE_PATH"] ?? defaultStatePath),
zshSessionsDirectory: URL(fileURLWithPath: environment["TERMINAL_GLOBAL_LOG_ZSH_SESSIONS_DIR"] ?? defaultSessionsPath, isDirectory: true),
snapshotFixturePath: environment["TERMINAL_GLOBAL_LOG_SNAPSHOT_FIXTURE"].map { URL(fileURLWithPath: $0) },
snapshotScriptPath: URL(fileURLWithPath: environment["TERMINAL_GLOBAL_LOG_SNAPSHOT_SCRIPT"] ?? defaultScriptPath),
byteLimit: byteLimit
)
}
}
struct CanonicalRecord: Codable {
let schemaVersion: Int
let recordID: String
let recordType: String
let sessionID: String
let sourceKind: String
let sourcePath: String
let sourceGeneration: Int
let sourceOffsetStart: UInt64
let sourceOffsetEnd: UInt64
let capturedAt: String
let sessionWindowID: Int?
let sessionTabID: Int?
let tty: String?
let hostname: String
let cwd: String?
let payload: String
enum CodingKeys: String, CodingKey {
case schemaVersion = "schema_version"
case recordID = "record_id"
case recordType = "record_type"
case sessionID = "session_id"
case sourceKind = "source_kind"
case sourcePath = "source_path"
case sourceGeneration = "source_generation"
case sourceOffsetStart = "source_offset_start"
case sourceOffsetEnd = "source_offset_end"
case capturedAt = "captured_at"
case sessionWindowID = "session_window_id"
case sessionTabID = "session_tab_id"
case tty
case hostname
case cwd
case payload
}
}
struct CollectorState: Codable {
var schemaVersion: Int = 1
var terminalSnapshots: [String: SnapshotCursor] = [:]
var sessionArtifacts: [String: ArtifactCursor] = [:]
enum CodingKeys: String, CodingKey {
case schemaVersion = "schema_version"
case terminalSnapshots = "terminal_snapshots"
case sessionArtifacts = "session_artifacts"
}
}
struct SnapshotCursor: Codable {
var payloadHash: String
var lastCapturedAt: String
}
struct ArtifactCursor: Codable {
var fingerprint: String
var generation: Int
var offset: UInt64
var lastSize: UInt64
var lastModified: TimeInterval
}
protocol HarvestSource {
func harvest(state: inout CollectorState, now: Date) throws -> [CanonicalRecord]
}
extension ISO8601DateFormatter {
static let terminalGlobalLog: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()
}
func sha256Hex(_ data: Data) -> String {
let digest = SHA256.hash(data: data)
return digest.map { String(format: "%02x", $0) }.joined()
}
func sha256Hex(_ string: String) -> String {
sha256Hex(Data(string.utf8))
}
func makeRecordID(
sourceKind: String,
sourcePath: String,
generation: Int,
start: UInt64,
end: UInt64,
payload: String
) -> String {
sha256Hex("\(sourceKind)|\(sourcePath)|\(generation)|\(start)|\(end)|\(sha256Hex(payload))")
}
#!/bin/zsh
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
VISIBILITY="${1:---public}"
DESC="${2:-Apple Terminal Global Log for macOS: a friendly, no-wrapper way to keep one capped JSONL history across Apple Terminal sessions without breaking session restore or your normal setup.}"
case "$VISIBILITY" in
--public|"")
VISIBILITY="--public"
;;
--secret)
VISIBILITY=""
;;
*)
echo "Usage: zsh publish-gist.sh [--public|--secret] [description]"
exit 1
;;
esac
cd "$SCRIPT_DIR"
cmd=(gh gist create)
if [[ -n "$VISIBILITY" ]]; then
cmd+=("$VISIBILITY")
fi
cmd+=(
-d "$DESC"
README.md
install.sh
uninstall.sh
publish-gist.sh
Models.swift
StateStore.swift
AppleTerminalSnapshotSource.swift
ZshSessionArtifactSource.swift
GlobalLogStore.swift
main.swift
dump_open_terminal_tabs.applescript
com.thedawgctor.terminal-global-log.plist.template
)
"${cmd[@]}"
import Foundation
final class StateStore {
private let statePath: URL
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
init(statePath: URL) {
self.statePath = statePath
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
}
func load() throws -> CollectorState {
guard FileManager.default.fileExists(atPath: statePath.path) else {
return CollectorState()
}
let data = try Data(contentsOf: statePath)
return try decoder.decode(CollectorState.self, from: data)
}
func save(_ state: CollectorState) throws {
let directory = statePath.deletingLastPathComponent()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
let tempURL = statePath.appendingPathExtension("tmp")
let data = try encoder.encode(state)
try data.write(to: tempURL, options: .atomic)
if FileManager.default.fileExists(atPath: statePath.path) {
try FileManager.default.removeItem(at: statePath)
}
try FileManager.default.moveItem(at: tempURL, to: statePath)
}
}
#!/bin/zsh
set -euo pipefail
APP_ROOT="$HOME/.terminal-global-log"
LAUNCHD_DST="$HOME/Library/LaunchAgents/com.thedawgctor.terminal-global-log.plist"
launchctl bootout "gui/$(id -u)" "$LAUNCHD_DST" 2>/dev/null || true
rm -f "$LAUNCHD_DST"
if [[ -f "$APP_ROOT/backups/com.apple.Terminal.before-install.plist" ]]; then
echo "Restore Terminal defaults with:"
echo "defaults import com.apple.Terminal \"$APP_ROOT/backups/com.apple.Terminal.before-install.plist\""
fi
rm -rf "$APP_ROOT"
rm -f "$HOME/.terminal_global_history.jsonl"
echo "Removed $APP_ROOT"
import Foundation
final class ZshSessionArtifactSource: HarvestSource {
private let config: CollectorConfig
private let interestingExtensions = Set(["history", "historynew", "session"])
init(config: CollectorConfig) {
self.config = config
}
func harvest(state: inout CollectorState, now: Date) throws -> [CanonicalRecord] {
guard FileManager.default.fileExists(atPath: config.zshSessionsDirectory.path) else {
return []
}
let urls = try FileManager.default.contentsOfDirectory(
at: config.zshSessionsDirectory,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
)
let timestamp = ISO8601DateFormatter.terminalGlobalLog.string(from: now)
var records: [CanonicalRecord] = []
for url in urls.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }) where interestingExtensions.contains(url.pathExtension) {
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
let size = (attributes[.size] as? NSNumber)?.uint64Value ?? 0
let modified = (attributes[.modificationDate] as? Date)?.timeIntervalSince1970 ?? 0
let data = try Data(contentsOf: url)
let fingerprintSeed = data.prefix(min(4096, data.count)) + Data(url.lastPathComponent.utf8)
let fingerprint = sha256Hex(fingerprintSeed)
let key = url.path
var cursor = state.sessionArtifacts[key] ?? ArtifactCursor(
fingerprint: fingerprint,
generation: 1,
offset: 0,
lastSize: 0,
lastModified: modified
)
if cursor.fingerprint != fingerprint || size < cursor.offset {
cursor.generation += 1
cursor.offset = 0
}
guard size > cursor.offset else {
cursor.fingerprint = fingerprint
cursor.lastSize = size
cursor.lastModified = modified
state.sessionArtifacts[key] = cursor
continue
}
let start = Int(cursor.offset)
let payloadData = data.subdata(in: start..<data.count)
let payload = String(decoding: payloadData, as: UTF8.self)
let sessionID = url.deletingPathExtension().lastPathComponent
let record = CanonicalRecord(
schemaVersion: 1,
recordID: makeRecordID(
sourceKind: "zsh_session_artifact",
sourcePath: url.path,
generation: cursor.generation,
start: cursor.offset,
end: size,
payload: payload
),
recordType: "zsh_session_artifact_chunk",
sessionID: sessionID,
sourceKind: "zsh_session_artifact",
sourcePath: url.path,
sourceGeneration: cursor.generation,
sourceOffsetStart: cursor.offset,
sourceOffsetEnd: size,
capturedAt: timestamp,
sessionWindowID: nil,
sessionTabID: nil,
tty: nil,
hostname: Host.current().localizedName ?? "unknown-host",
cwd: nil,
payload: payload
)
records.append(record)
cursor.fingerprint = fingerprint
cursor.offset = size
cursor.lastSize = size
cursor.lastModified = modified
state.sessionArtifacts[key] = cursor
}
return records
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment