@searls/turbocommit 0.13.2 → 0.14.1

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/README.md CHANGED
@@ -32,6 +32,30 @@ turbocommit registers hooks with the harnesses you use:
32
32
  commits with `git add -A`. If not, it buffers the transcript for pickup
33
33
  by a later session that does commit.
34
34
 
35
+ ### Changes across multiple repositories
36
+
37
+ A session that starts in an enabled repository can commit changes in other
38
+ enabled local checkouts during the same turn. Turbocommit discovers each
39
+ checkout from explicit file paths supplied to Claude Code tools, Codex
40
+ `apply_patch`, and path-bearing MCP tools. Shell commands alone do not discover
41
+ additional repositories.
42
+
43
+ Each touched checkout:
44
+
45
+ - Reads its own merged global and project configuration.
46
+ - Keeps the normal `git add -A` behavior.
47
+ - Gets an independent commit with the full turn transcript.
48
+ - Gives the configured title agent repository-specific bounded diff context.
49
+
50
+ Turbocommit attempts every commit before pushing every successful commit whose
51
+ repository has `push: true`. Failures are retained and retried on a later Stop,
52
+ along with every transcript accumulated before the commit succeeds.
53
+
54
+ Separate worktrees are committed independently. Enabled submodules are
55
+ committed and pushed before enabled parent repositories. When one turn creates
56
+ commits in multiple repositories, each commit includes a shared
57
+ `Turbocommit-Session` trailer.
58
+
35
59
  The commit message headline is generated by a title agent (configurable).
36
60
  The body contains the full prompt/response transcript. When planning
37
61
  context was buffered from ancestor sessions, it appears under a
package/lib/git.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { execSync } = require('child_process')
2
+ const fs = require('fs')
2
3
  const path = require('path')
3
4
 
4
5
  function git (args, opts = {}) {
@@ -18,6 +19,21 @@ function gitRoot (cwd) {
18
19
  }
19
20
  }
20
21
 
22
+ function gitRootForPath (filePath) {
23
+ let probe = filePath
24
+ try {
25
+ if (!fs.statSync(probe).isDirectory()) probe = path.dirname(probe)
26
+ } catch {
27
+ probe = path.dirname(probe)
28
+ }
29
+ while (probe && !fs.existsSync(probe)) {
30
+ const parent = path.dirname(probe)
31
+ if (parent === probe) return null
32
+ probe = parent
33
+ }
34
+ return gitRoot(probe)
35
+ }
36
+
21
37
  function gitCommonDir (cwd) {
22
38
  try {
23
39
  const out = git('rev-parse --git-common-dir', { cwd })
@@ -45,11 +61,41 @@ function hasChanges (cwd) {
45
61
  }
46
62
 
47
63
  function addAndCommit (cwd, headline, body) {
64
+ stageAll(cwd)
65
+ commitStaged(cwd, headline, body)
66
+ return git('rev-parse HEAD', { cwd })
67
+ }
68
+
69
+ function stageAll (cwd) {
48
70
  git('add -A', { cwd })
71
+ }
72
+
73
+ function commitStaged (cwd, headline, body) {
49
74
  git(`commit -m "${esc(headline)}" -m "${esc(body)}" --no-verify`, { cwd })
50
75
  return git('rev-parse HEAD', { cwd })
51
76
  }
52
77
 
78
+ function stagedChangeContext (cwd, budget = 20000) {
79
+ const status = git('status --short', { cwd })
80
+ const stat = git('diff --cached --stat', { cwd })
81
+ const diff = git('diff --cached --no-ext-diff --unified=3', { cwd })
82
+ const prefix = `Status:\n${status || '(clean)'}\n\nDiff stat:\n${stat || '(none)'}\n\nDiff:\n`
83
+ const remaining = Math.max(0, budget - prefix.length)
84
+ return prefix + (diff.length > remaining ? diff.slice(0, remaining) + '\n[... diff truncated ...]' : diff)
85
+ }
86
+
87
+ function isGitlink (parentRoot, childRoot) {
88
+ const relative = path.relative(parentRoot, childRoot)
89
+ if (!relative || relative.startsWith('..')) return false
90
+ try {
91
+ return git(`ls-files --stage -- "${esc(relative)}"`, { cwd: parentRoot })
92
+ .split('\n')
93
+ .some(line => line.startsWith('160000 '))
94
+ } catch {
95
+ return false
96
+ }
97
+ }
98
+
53
99
  function hasCommits (cwd) {
54
100
  try {
55
101
  git('rev-parse HEAD', { cwd })
@@ -83,10 +129,15 @@ function esc (s) {
83
129
  module.exports = {
84
130
  git,
85
131
  gitRoot,
132
+ gitRootForPath,
86
133
  gitCommonDir,
87
134
  hasChanges,
88
135
  addAndCommit,
136
+ stageAll,
137
+ commitStaged,
138
+ stagedChangeContext,
89
139
  hasCommits,
90
140
  currentBranch,
91
- pushClean
141
+ pushClean,
142
+ isGitlink
92
143
  }
package/lib/install.js CHANGED
@@ -23,7 +23,7 @@ const HOOK_DEFS = {
23
23
 
24
24
  const CODEX_HOOK_DEFS = {
25
25
  PreToolUse: {
26
- matcher: 'Write|Edit|MultiEdit|NotebookEdit|Bash|mcp__.*',
26
+ matcher: 'apply_patch|Write|Edit|MultiEdit|NotebookEdit|Bash|mcp__.*',
27
27
  hooks: [{ type: 'command', command: 'turbocommit hook pre-tool-use --harness codex' }]
28
28
  },
29
29
  SessionStart: {
package/lib/multi.js ADDED
@@ -0,0 +1,121 @@
1
+ const crypto = require('crypto')
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+ const { ensureDir, loadJson } = require('./io')
5
+ const { turbocommitDir } = require('./session')
6
+ const { gitRoot, gitRootForPath, isGitlink } = require('./git')
7
+ const { activeConfig } = require('./config')
8
+
9
+ function checkoutKey (root) {
10
+ return crypto.createHash('sha256').update(fs.realpathSync(root)).digest('hex').slice(0, 16)
11
+ }
12
+
13
+ function manifestPath (anchor) {
14
+ return path.join(turbocommitDir(anchor), 'multi', checkoutKey(anchor), 'manifest.json')
15
+ }
16
+
17
+ function loadManifest (anchor) {
18
+ return loadJson(manifestPath(anchor)) || { nextOrder: 0, repos: [] }
19
+ }
20
+
21
+ function saveManifest (anchor, manifest) {
22
+ const file = manifestPath(anchor)
23
+ ensureDir(path.dirname(file))
24
+ fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + '\n')
25
+ }
26
+
27
+ function deleteManifestIfEmpty (anchor, manifest) {
28
+ if (manifest.repos.length > 0) {
29
+ saveManifest(anchor, manifest)
30
+ return
31
+ }
32
+ try {
33
+ fs.unlinkSync(manifestPath(anchor))
34
+ } catch {}
35
+ }
36
+
37
+ function rootsFromEntries (anchor, entries) {
38
+ const roots = []
39
+ const seen = new Set()
40
+ const add = root => {
41
+ if (!root) return
42
+ let real
43
+ try {
44
+ real = fs.realpathSync(root)
45
+ } catch {
46
+ return
47
+ }
48
+ if (seen.has(real)) return
49
+ if (activeConfig(real).config.enabled !== true) return
50
+ seen.add(real)
51
+ roots.push(real)
52
+ }
53
+
54
+ for (const entry of entries) {
55
+ if (entry.tool === 'Bash') continue
56
+ if (Array.isArray(entry.files) && entry.files.length > 0) {
57
+ for (const file of entry.files) add(gitRootForPath(file) || anchor)
58
+ } else {
59
+ add(anchor)
60
+ }
61
+ }
62
+ return addEnabledSuperprojects(roots, add)
63
+ }
64
+
65
+ function addEnabledSuperprojects (roots, add) {
66
+ for (let i = 0; i < roots.length; i++) {
67
+ const child = roots[i]
68
+ const parent = gitRoot(path.dirname(child))
69
+ if (parent && parent !== child && isGitlink(parent, child)) add(parent)
70
+ }
71
+ return roots
72
+ }
73
+
74
+ function dependencyOrder (repos) {
75
+ return [...repos].sort((a, b) => {
76
+ if (isGitlink(a.root, b.root)) return 1
77
+ if (isGitlink(b.root, a.root)) return -1
78
+ return a.order - b.order
79
+ })
80
+ }
81
+
82
+ function appendWork (manifest, root, work) {
83
+ let repo = manifest.repos.find(candidate => candidate.root === root)
84
+ if (!repo) {
85
+ repo = { root, order: manifest.nextOrder++, work: [] }
86
+ manifest.repos.push(repo)
87
+ }
88
+ if (!repo.work.some(item => item.key === work.key)) repo.work.push(work)
89
+ return repo
90
+ }
91
+
92
+ function multiWatermarkPath (root, sessionId) {
93
+ return path.join(turbocommitDir(root), 'multi-watermarks', checkoutKey(root), sessionId + '.json')
94
+ }
95
+
96
+ function readMultiWatermark (root, sessionIds) {
97
+ for (const sessionId of sessionIds) {
98
+ const value = loadJson(multiWatermarkPath(root, sessionId))
99
+ if (value?.commit) return value
100
+ }
101
+ return null
102
+ }
103
+
104
+ function saveMultiWatermark (root, sessionId, commit) {
105
+ const file = multiWatermarkPath(root, sessionId)
106
+ ensureDir(path.dirname(file))
107
+ fs.writeFileSync(file, JSON.stringify({ commit }) + '\n')
108
+ }
109
+
110
+ module.exports = {
111
+ appendWork,
112
+ checkoutKey,
113
+ deleteManifestIfEmpty,
114
+ dependencyOrder,
115
+ loadManifest,
116
+ manifestPath,
117
+ readMultiWatermark,
118
+ rootsFromEntries,
119
+ saveManifest,
120
+ saveMultiWatermark
121
+ }
package/lib/run.js CHANGED
@@ -4,15 +4,25 @@ const path = require('path')
4
4
  const { loadJson } = require('./io')
5
5
  const { parseTranscript, parseCodexTranscript, formatBody, formatTitleTranscript, extractHeadline, extractModel, extractCodexModel } = require('./transcript')
6
6
  const { runTitleAgent, runBodyAgent } = require('./agent')
7
- const { gitRoot, hasChanges, addAndCommit, hasCommits, currentBranch, pushClean } = require('./git')
7
+ const { gitRoot, hasChanges, addAndCommit, stageAll, commitStaged, stagedChangeContext, hasCommits, currentBranch, pushClean } = require('./git')
8
8
  const { logEvent } = require('./log')
9
9
  const { wrapText } = require('./wrap')
10
- const { hasTrackedModifications, cleanupTracking } = require('./track')
10
+ const { hasTrackedModifications, cleanupTracking, readTracking } = require('./track')
11
11
  const { redact, buildRedactions } = require('./redact')
12
12
  const { handleSessionEnd, getAncestors, savePending, collectPending, cleanupConsumed, cleanupStale, readWatermark, saveWatermark, resolveParentCommit } = require('./session')
13
13
  const { activeConfig } = require('./config')
14
14
  const { normalizeHookInput } = require('./harness')
15
15
  const { resolveCodexTranscriptPath } = require('./codex')
16
+ const {
17
+ appendWork,
18
+ deleteManifestIfEmpty,
19
+ dependencyOrder,
20
+ loadManifest,
21
+ readMultiWatermark,
22
+ rootsFromEntries,
23
+ saveManifest,
24
+ saveMultiWatermark
25
+ } = require('./multi')
16
26
 
17
27
  /**
18
28
  * Map a model ID like "claude-opus-4-6" to a friendly name like "Claude Opus 4.6".
@@ -92,6 +102,29 @@ function resolveCoauthor (config, transcriptPath, root, opts = {}) {
92
102
  */
93
103
  function run (input, opts = {}) {
94
104
  if (process.env.TURBOCOMMIT_DISABLED) return
105
+ const hookInput = typeof input === 'string'
106
+ ? normalizeHookInput(input, 'stop', opts.harness)
107
+ : input
108
+ if (!hookInput) return
109
+
110
+ const root = gitRoot(hookInput.cwd || process.cwd())
111
+ if (!root || activeConfig(root).config.enabled !== true) return
112
+
113
+ const entries = hookInput.sessionId ? readTracking(root, hookInput.sessionId) : []
114
+ const roots = rootsFromEntries(root, entries)
115
+ const manifest = loadManifest(root)
116
+ const hasCrossRootPath = entries.some(entry =>
117
+ Array.isArray(entry.files) && entry.files.length > 0
118
+ ) && (roots.length !== 1 || roots[0] !== root)
119
+
120
+ if (manifest.repos.length > 0 || roots.length > 1 || hasCrossRootPath) {
121
+ return runMulti(hookInput, root, roots, manifest)
122
+ }
123
+ return runSingle(hookInput)
124
+ }
125
+
126
+ function runSingle (input, opts = {}) {
127
+ if (process.env.TURBOCOMMIT_DISABLED) return
95
128
 
96
129
  const hookInput = typeof input === 'string'
97
130
  ? normalizeHookInput(input, 'stop', opts.harness)
@@ -240,6 +273,191 @@ function run (input, opts = {}) {
240
273
  }
241
274
  }
242
275
 
276
+ function runMulti (hookInput, anchor, currentRoots, manifest) {
277
+ recordCodexStop(hookInput, anchor)
278
+
279
+ const transcriptPath = resolveTranscriptPath(hookInput)
280
+ const pairs = hookInput.harness === 'codex'
281
+ ? parseCodexTranscript(transcriptPath)
282
+ : parseTranscript(transcriptPath)
283
+ const sessionId = hookInput.sessionId
284
+ const watermark = sessionId ? readWatermark(anchor, sessionId) : null
285
+ const watermarkPairCount = watermark && Number.isInteger(watermark.pairs) ? watermark.pairs : 0
286
+ const newPairs = watermark ? pairs.slice(watermarkPairCount) : pairs
287
+ const precompactWatermark = watermark && watermark.source === 'precompact'
288
+ const selfPrecompactPending = sessionId
289
+ ? collectPending(anchor, [sessionId], { source: 'precompact' })
290
+ : []
291
+ let effectivePairs = newPairs.length > 0 ? newPairs : pairs
292
+ if (precompactWatermark && newPairs.length === 0 && selfPrecompactPending.length > 0) {
293
+ const basePairs = Number.isInteger(watermark.basePairs) ? watermark.basePairs : 0
294
+ const bufferedPairs = pairs.slice(basePairs, watermarkPairCount)
295
+ effectivePairs = bufferedPairs.length > 0 ? bufferedPairs : pairs
296
+ }
297
+ const formattedTranscript = formatBody(effectivePairs)
298
+ const ancestors = sessionId ? getAncestors(anchor, sessionId) : []
299
+ const pending = sessionId ? collectPending(anchor, [...ancestors].reverse()) : []
300
+ if (precompactWatermark && newPairs.length > 0 && selfPrecompactPending.length > 0) {
301
+ pending.push(...selfPrecompactPending)
302
+ }
303
+ const turnBody = pending.length > 0
304
+ ? '## Planning\n\n' + pending.join('\n\n---\n\n') +
305
+ '\n\n## Implementation\n\n' + formattedTranscript
306
+ : formattedTranscript
307
+
308
+ const dirtyRoots = currentRoots.filter(root => !hasCommits(root) || hasChanges(root))
309
+ const isMultiTurn = currentRoots.length > 1
310
+ const turnKey = hookInput.raw?.turn_id ||
311
+ cryptoKey(`${sessionId || 'no-session'}\n${pairs.length}\n${turnBody}`)
312
+
313
+ if (dirtyRoots.length === 0 && manifest.repos.length === 0) {
314
+ if (sessionId) {
315
+ savePending(anchor, sessionId, formattedTranscript)
316
+ cleanupTracking(anchor, sessionId)
317
+ }
318
+ logEvent('skip', {
319
+ harness: hookInput.harness,
320
+ project: path.basename(anchor),
321
+ branch: currentBranch(anchor),
322
+ context: transcriptSize(transcriptPath)
323
+ })
324
+ return
325
+ }
326
+
327
+ for (const root of currentRoots) {
328
+ appendWork(manifest, root, {
329
+ key: turnKey,
330
+ body: turnBody,
331
+ sessionIds: isMultiTurn && sessionId ? [sessionId] : []
332
+ })
333
+ }
334
+
335
+ saveManifest(anchor, manifest)
336
+ const successes = []
337
+ const ordered = dependencyOrder(manifest.repos)
338
+
339
+ for (const repo of ordered) {
340
+ const root = repo.root
341
+ const { config } = activeConfig(root)
342
+ const project = path.basename(root)
343
+ const branch = currentBranch(root)
344
+ const context = transcriptSize(transcriptPath)
345
+
346
+ if (config.enabled !== true) {
347
+ manifest.repos = manifest.repos.filter(candidate => candidate !== repo)
348
+ logEvent('skip', { harness: hookInput.harness, project, branch, context })
349
+ continue
350
+ }
351
+ if (hasCommits(root) && !hasChanges(root)) {
352
+ manifest.repos = manifest.repos.filter(candidate => candidate !== repo)
353
+ logEvent('skip', { harness: hookInput.harness, project, branch, context })
354
+ continue
355
+ }
356
+
357
+ try {
358
+ logEvent('start', { harness: hookInput.harness, project, branch, context })
359
+ stageAll(root)
360
+
361
+ const rawBody = repo.work.map(item => item.body).join('\n\n---\n\n')
362
+ const redactions = buildRedactions()
363
+ const changeContext = redact(stagedChangeContext(root, 10000), redactions)
364
+ const titleInput = `${changeContext}\n\nTranscript:\n${rawBody}`.slice(0, 20000)
365
+
366
+ let headline
367
+ if (config.title?.type !== 'transcript') {
368
+ headline = runTitleAgent(root, config.title || {}, titleInput, hookInput.harness)
369
+ }
370
+ headline = headline || extractHeadline(effectivePairs)
371
+
372
+ let body
373
+ if (config.body?.type === 'agent') {
374
+ body = runBodyAgent(root, config.body, rawBody, hookInput.harness)
375
+ }
376
+ body = body || rawBody
377
+
378
+ const parent = sessionId
379
+ ? readMultiWatermark(root, [sessionId, ...ancestors])
380
+ : null
381
+ if (parent?.commit) body = `Continuation of ${parent.commit.slice(0, 7)}\n\n${body}`
382
+ body = wrapText(body, config.body?.maxLineLength)
383
+
384
+ const trailers = []
385
+ for (const work of repo.work) {
386
+ for (const id of work.sessionIds || []) {
387
+ if (!trailers.includes(id)) trailers.push(id)
388
+ }
389
+ }
390
+ if (trailers.length > 0) {
391
+ body += '\n\n' + trailers.map(id => `Turbocommit-Session: ${id}`).join('\n')
392
+ }
393
+
394
+ const coauthor = resolveCoauthor(config, transcriptPath, root, {
395
+ harness: hookInput.harness,
396
+ model: hookInput.model
397
+ })
398
+ if (coauthor) body += '\n\n' + coauthor
399
+
400
+ const safeHeadline = redact(headline, redactions)
401
+ const safeBody = redact(body, redactions)
402
+ const sha = commitStaged(root, safeHeadline, safeBody)
403
+ if (sessionId) saveMultiWatermark(root, sessionId, sha)
404
+ if (root === anchor && sessionId) {
405
+ saveWatermark(anchor, sessionId, pairs.length, sha, { source: 'commit' })
406
+ }
407
+ manifest.repos = manifest.repos.filter(candidate => candidate !== repo)
408
+ successes.push({ root, config, project, branch })
409
+ logEvent('success', {
410
+ harness: hookInput.harness,
411
+ project,
412
+ branch,
413
+ context,
414
+ title: safeHeadline
415
+ })
416
+ } catch {
417
+ logEvent('fail', { harness: hookInput.harness, project, branch, context })
418
+ }
419
+ }
420
+
421
+ for (const success of successes) {
422
+ if (success.config.push !== true) continue
423
+ if (pushClean(success.root)) {
424
+ logEvent('push', {
425
+ harness: hookInput.harness,
426
+ project: success.project,
427
+ branch: success.branch
428
+ })
429
+ } else {
430
+ logEvent('push-fail', {
431
+ harness: hookInput.harness,
432
+ project: success.project,
433
+ branch: success.branch
434
+ })
435
+ }
436
+ }
437
+
438
+ if (sessionId) {
439
+ if (!readWatermark(anchor, sessionId)) {
440
+ saveWatermark(anchor, sessionId, pairs.length, undefined, { source: 'multi' })
441
+ }
442
+ cleanupConsumed(anchor, [...ancestors, sessionId])
443
+ cleanupTracking(anchor, sessionId)
444
+ }
445
+ deleteManifestIfEmpty(anchor, manifest)
446
+ cleanupStale(anchor)
447
+ }
448
+
449
+ function transcriptSize (transcriptPath) {
450
+ try {
451
+ return fs.statSync(transcriptPath).size
452
+ } catch {
453
+ return 0
454
+ }
455
+ }
456
+
457
+ function cryptoKey (value) {
458
+ return require('crypto').createHash('sha256').update(value).digest('hex').slice(0, 20)
459
+ }
460
+
243
461
  function runPreCompact (input, opts = {}) {
244
462
  if (process.env.TURBOCOMMIT_DISABLED) return
245
463
  const hookInput = typeof input === 'string'
package/lib/track.js CHANGED
@@ -36,6 +36,41 @@ function extractFilePath (toolInput) {
36
36
  return null
37
37
  }
38
38
 
39
+ function extractFilePaths (toolName, toolInput, cwd) {
40
+ const paths = []
41
+ const seen = new Set()
42
+ const add = value => {
43
+ if (typeof value !== 'string' || value.length === 0) return
44
+ const resolved = path.isAbsolute(value) ? value : path.resolve(cwd || process.cwd(), value)
45
+ if (seen.has(resolved)) return
46
+ seen.add(resolved)
47
+ paths.push(resolved)
48
+ }
49
+
50
+ if (toolName === 'apply_patch' && typeof toolInput?.command === 'string') {
51
+ for (const line of toolInput.command.split('\n')) {
52
+ const match = line.match(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/) ||
53
+ line.match(/^\*\*\* Move to: (.+)$/)
54
+ if (match) add(match[1])
55
+ }
56
+ return paths
57
+ }
58
+
59
+ const visit = value => {
60
+ if (!value || typeof value !== 'object') return
61
+ if (Array.isArray(value)) {
62
+ for (const item of value) visit(item)
63
+ return
64
+ }
65
+ for (const [key, nested] of Object.entries(value)) {
66
+ if (FILE_PATH_KEYS.includes(key)) add(nested)
67
+ if (nested && typeof nested === 'object') visit(nested)
68
+ }
69
+ }
70
+ visit(toolInput)
71
+ return paths
72
+ }
73
+
39
74
  /**
40
75
  * PreToolUse handler. Appends a tracking entry for potentially-modifying tools.
41
76
  * Always exits 0 (never blocks tool execution).
@@ -55,11 +90,9 @@ function handleTrack (input, root) {
55
90
 
56
91
  const entry = { tool: toolName, t: Date.now() }
57
92
 
58
- // Extract file path for file-modifying tools
59
- const filePath = extractFilePath(toolInput)
60
- if (filePath) {
61
- entry.file = filePath
62
- }
93
+ const cwd = hookInput.cwd || hookInput.raw?.cwd || root
94
+ const files = extractFilePaths(toolName, toolInput, cwd)
95
+ if (files.length > 0) entry.files = files
63
96
 
64
97
  // For Bash, record the command
65
98
  if (toolName === 'Bash' && typeof toolInput.command === 'string') {
@@ -92,21 +125,23 @@ function parseInput (input) {
92
125
  * The definitive signal comes from Write/Edit/NotebookEdit/MCP tools.
93
126
  */
94
127
  function hasTrackedModifications (root, sessionId) {
128
+ return readTracking(root, sessionId).some(entry => entry.tool !== 'Bash')
129
+ }
130
+
131
+ function readTracking (root, sessionId) {
95
132
  const file = trackingPath(root, sessionId)
96
133
  try {
97
134
  const data = fs.readFileSync(file, 'utf8')
98
- if (!data) return false
99
- const lines = data.trim().split('\n')
100
- return lines.some(line => {
135
+ if (!data) return []
136
+ return data.trim().split('\n').map(line => {
101
137
  try {
102
- const entry = JSON.parse(line)
103
- return entry.tool !== 'Bash'
138
+ return JSON.parse(line)
104
139
  } catch {
105
- return false
140
+ return null
106
141
  }
107
- })
142
+ }).filter(Boolean)
108
143
  } catch {
109
- return false
144
+ return []
110
145
  }
111
146
  }
112
147
 
@@ -124,6 +159,8 @@ module.exports = {
124
159
  hasTrackedModifications,
125
160
  cleanupTracking,
126
161
  extractFilePath,
162
+ extractFilePaths,
163
+ readTracking,
127
164
  trackingDir,
128
165
  trackingPath
129
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@searls/turbocommit",
3
- "version": "0.13.2",
3
+ "version": "0.14.1",
4
4
  "description": "Auto-commit after every AI coding agent turn",
5
5
  "bin": {
6
6
  "turbocommit": "./cli.js"