forked from ultraworkers/claw-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoutputsScanner.ts
More file actions
126 lines (113 loc) · 3.6 KB
/
Copy pathoutputsScanner.ts
File metadata and controls
126 lines (113 loc) · 3.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/**
* Outputs directory scanner for file persistence
*
* This module provides utilities to:
* - Detect the session type from environment variables
* - Capture turn start timestamp
* - Find modified files by comparing file mtimes against turn start time
*/
import * as fs from 'fs/promises'
import * as path from 'path'
import { logForDebugging } from '../debug.js'
import type { EnvironmentKind } from '../teleport/environments.js'
import type { TurnStartTime } from './types.js'
/** Shared debug logger for file persistence modules */
export function logDebug(message: string): void {
logForDebugging(`[file-persistence] ${message}`)
}
/**
* Get the environment kind from CLAUDE_CODE_ENVIRONMENT_KIND.
* Returns null if not set or not a recognized value.
*/
export function getEnvironmentKind(): EnvironmentKind | null {
const kind = process.env.CLAUDE_CODE_ENVIRONMENT_KIND
if (kind === 'byoc' || kind === 'anthropic_cloud') {
return kind
}
return null
}
function hasParentPath(
entry: object,
): entry is { parentPath: string; name: string } {
return 'parentPath' in entry && typeof entry.parentPath === 'string'
}
function hasPath(entry: object): entry is { path: string; name: string } {
return 'path' in entry && typeof entry.path === 'string'
}
function getEntryParentPath(entry: object, fallback: string): string {
if (hasParentPath(entry)) {
return entry.parentPath
}
if (hasPath(entry)) {
return entry.path
}
return fallback
}
/**
* Find files that have been modified since the turn started.
* Returns paths of files with mtime >= turnStartTime.
*
* Uses recursive directory listing and parallelized stat calls for efficiency.
*
* @param turnStartTime - The timestamp when the turn started
* @param outputsDir - The directory to scan for modified files
*/
export async function findModifiedFiles(
turnStartTime: TurnStartTime,
outputsDir: string,
): Promise<string[]> {
// Use recursive flag to get all entries in one call
let entries: Awaited<ReturnType<typeof fs.readdir>>
try {
entries = await fs.readdir(outputsDir, {
withFileTypes: true,
recursive: true,
})
} catch {
// Directory doesn't exist or is not accessible
return []
}
// Filter to regular files only (skip symlinks for security) and build full paths
const filePaths: string[] = []
for (const entry of entries) {
if (entry.isSymbolicLink()) {
continue
}
if (entry.isFile()) {
// entry.parentPath is available in Node 20+, fallback to entry.path for older versions
const parentPath = getEntryParentPath(entry, outputsDir)
filePaths.push(path.join(parentPath, entry.name))
}
}
if (filePaths.length === 0) {
logDebug('No files found in outputs directory')
return []
}
// Parallelize stat calls for all files
const statResults = await Promise.all(
filePaths.map(async filePath => {
try {
const stat = await fs.lstat(filePath)
// Skip if it became a symlink between readdir and stat (race condition)
if (stat.isSymbolicLink()) {
return null
}
return { filePath, mtimeMs: stat.mtimeMs }
} catch {
// File may have been deleted between readdir and stat
return null
}
}),
)
// Filter to files modified since turn start
const modifiedFiles: string[] = []
for (const result of statResults) {
if (result && result.mtimeMs >= turnStartTime) {
modifiedFiles.push(result.filePath)
}
}
logDebug(
`Found ${modifiedFiles.length} modified files since turn start (scanned ${filePaths.length} total)`,
)
return modifiedFiles
}