@searls/turbocommit 0.12.1 → 0.13.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.
package/lib/session.js CHANGED
@@ -39,14 +39,10 @@ function watermarkDir (root) {
39
39
  function handleSessionEnd (input, root) {
40
40
  if (!root) return
41
41
 
42
- let hookInput
43
- try {
44
- hookInput = JSON.parse(input)
45
- } catch {
46
- return
47
- }
42
+ const hookInput = typeof input === 'string' ? parseInput(input) : input
43
+ if (!hookInput) return
48
44
 
49
- const sessionId = hookInput.session_id
45
+ const sessionId = hookInput.sessionId || hookInput.session_id
50
46
  if (!sessionId) return
51
47
 
52
48
  const dir = breadcrumbDir(root)
@@ -62,14 +58,10 @@ function handleSessionEnd (input, root) {
62
58
  function handleSessionStart (input, root) {
63
59
  if (!root) return
64
60
 
65
- let hookInput
66
- try {
67
- hookInput = JSON.parse(input)
68
- } catch {
69
- return
70
- }
61
+ const hookInput = typeof input === 'string' ? parseInput(input) : input
62
+ if (!hookInput) return
71
63
 
72
- const sessionId = hookInput.session_id
64
+ const sessionId = hookInput.sessionId || hookInput.session_id
73
65
  if (!sessionId) return
74
66
 
75
67
  const source = hookInput.source
@@ -123,6 +115,14 @@ function handleSessionStart (input, root) {
123
115
  fs.writeFileSync(path.join(cDir, sessionId + '.json'), JSON.stringify(chain) + '\n')
124
116
  }
125
117
 
118
+ function parseInput (input) {
119
+ try {
120
+ return JSON.parse(input)
121
+ } catch {
122
+ return null
123
+ }
124
+ }
125
+
126
126
  function readChain (root, sessionId) {
127
127
  try {
128
128
  return JSON.parse(fs.readFileSync(path.join(chainDir(root), sessionId + '.json'), 'utf8'))
@@ -143,23 +143,25 @@ function getAncestors (root, sessionId) {
143
143
  * Save formatted transcript to pending directory for later pickup.
144
144
  */
145
145
  let pendingSeq = 0
146
- function savePending (root, sessionId, transcript) {
146
+ function savePending (root, sessionId, transcript, opts = {}) {
147
147
  const base = pendingDir(root)
148
148
  if (!base) return
149
149
  const dir = path.join(base, sessionId)
150
150
  ensureDir(dir)
151
151
  const timestamp = String(Date.now()) + '-' + String(pendingSeq++).padStart(4, '0')
152
- fs.writeFileSync(path.join(dir, timestamp + '.txt'), transcript)
152
+ const source = opts.source ? '.' + String(opts.source).replace(/[^a-z0-9-]/gi, '-') : ''
153
+ fs.writeFileSync(path.join(dir, timestamp + source + '.txt'), transcript)
153
154
  }
154
155
 
155
156
  /**
156
157
  * Collect pending transcripts for a list of session IDs, in order
157
158
  * (oldest ancestor first). Returns array of strings.
158
159
  */
159
- function collectPending (root, sessionIds) {
160
+ function collectPending (root, sessionIds, opts = {}) {
160
161
  const results = []
161
162
  const base = pendingDir(root)
162
163
  if (!base) return results
164
+ const sourceSuffix = opts.source ? '.' + opts.source + '.txt' : null
163
165
  for (const sid of sessionIds) {
164
166
  const dir = path.join(base, sid)
165
167
  let files
@@ -170,6 +172,7 @@ function collectPending (root, sessionIds) {
170
172
  }
171
173
  for (const file of files) {
172
174
  if (!file.endsWith('.txt')) continue
175
+ if (sourceSuffix && !file.endsWith(sourceSuffix)) continue
173
176
  try {
174
177
  const content = fs.readFileSync(path.join(dir, file), 'utf8')
175
178
  if (content.trim()) results.push(content)
@@ -195,11 +198,14 @@ function readWatermark (root, sessionId) {
195
198
  /**
196
199
  * Save watermark after a commit: pair count and commit SHA.
197
200
  */
198
- function saveWatermark (root, sessionId, pairs, commit) {
201
+ function saveWatermark (root, sessionId, pairs, commit, meta = {}) {
199
202
  const dir = watermarkDir(root)
200
203
  if (!dir) return
201
204
  ensureDir(dir)
202
- fs.writeFileSync(path.join(dir, sessionId + '.json'), JSON.stringify({ pairs, commit }) + '\n')
205
+ const data = { pairs }
206
+ if (commit) data.commit = commit
207
+ Object.assign(data, meta)
208
+ fs.writeFileSync(path.join(dir, sessionId + '.json'), JSON.stringify(data) + '\n')
203
209
  }
204
210
 
205
211
  /**
@@ -209,13 +215,13 @@ function saveWatermark (root, sessionId, pairs, commit) {
209
215
  function resolveParentCommit (root, sessionId) {
210
216
  // Own watermark takes priority (same-session previous commit)
211
217
  const own = readWatermark(root, sessionId)
212
- if (own) return own.commit
218
+ if (own && own.commit) return own.commit
213
219
 
214
220
  // Walk chain ancestors nearest-first
215
221
  const ancestors = getAncestors(root, sessionId)
216
222
  for (const aid of ancestors) {
217
223
  const wm = readWatermark(root, aid)
218
- if (wm) return wm.commit
224
+ if (wm && wm.commit) return wm.commit
219
225
  }
220
226
  return null
221
227
  }
package/lib/track.js CHANGED
@@ -41,22 +41,17 @@ function extractFilePath (toolInput) {
41
41
  * Always exits 0 (never blocks tool execution).
42
42
  */
43
43
  function handleTrack (input, root) {
44
- if (!root) return
44
+ const hookInput = typeof input === 'string' ? parseInput(input) : input
45
+ root = root || hookInput?.root
46
+ if (!root || !hookInput) return
45
47
 
46
- let hookInput
47
- try {
48
- hookInput = JSON.parse(input)
49
- } catch {
50
- return
51
- }
52
-
53
- const sessionId = hookInput.session_id
48
+ const sessionId = hookInput.sessionId || hookInput.session_id
54
49
  if (!sessionId) return
55
50
 
56
- const toolName = hookInput.tool_name
51
+ const toolName = hookInput.toolName || hookInput.tool_name
57
52
  if (!toolName) return
58
53
 
59
- const toolInput = hookInput.tool_input || {}
54
+ const toolInput = hookInput.toolInput || hookInput.tool_input || {}
60
55
 
61
56
  const entry = { tool: toolName, t: Date.now() }
62
57
 
@@ -82,6 +77,14 @@ function handleTrack (input, root) {
82
77
  fs.appendFileSync(file, JSON.stringify(entry) + '\n')
83
78
  }
84
79
 
80
+ function parseInput (input) {
81
+ try {
82
+ return JSON.parse(input)
83
+ } catch {
84
+ return null
85
+ }
86
+ }
87
+
85
88
  /**
86
89
  * Check whether a session has tracked any file-modifying tool calls.
87
90
  * Bash entries alone don't count — Bash is too noisy (ls, git status, etc.)
package/lib/transcript.js CHANGED
@@ -42,6 +42,58 @@ function parseTranscript (filePath) {
42
42
  return state.pairs
43
43
  }
44
44
 
45
+ function parseCodexTranscript (filePath) {
46
+ if (!filePath || !fs.existsSync(filePath)) return []
47
+
48
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n')
49
+ const state = { pairs: [], prompt: null, response: '' }
50
+
51
+ for (const line of lines) {
52
+ if (!line.trim()) continue
53
+ let entry
54
+ try {
55
+ entry = JSON.parse(line)
56
+ } catch {
57
+ continue
58
+ }
59
+
60
+ const payload = entry.payload
61
+ if (entry.type !== 'response_item' || payload?.type !== 'message') continue
62
+
63
+ if (payload.role === 'user') {
64
+ const prompt = visibleText(payload.content)
65
+ if (isCodexSystemStatus(prompt)) continue
66
+ if (state.prompt !== null) {
67
+ state.pairs.push({ prompt: state.prompt, response: state.response })
68
+ }
69
+ state.prompt = prompt
70
+ state.response = ''
71
+ } else if (payload.role === 'assistant' && state.prompt !== null) {
72
+ state.response += visibleText(payload.content)
73
+ }
74
+ }
75
+
76
+ if (state.prompt !== null) {
77
+ state.pairs.push({ prompt: state.prompt, response: state.response })
78
+ }
79
+
80
+ return state.pairs.filter(p => p.prompt)
81
+ }
82
+
83
+ function visibleText (content) {
84
+ if (typeof content === 'string') return content
85
+ if (!Array.isArray(content)) return ''
86
+ return content
87
+ .filter(block => block.type === 'input_text' || block.type === 'output_text' || block.type === 'text')
88
+ .map(block => block.text)
89
+ .filter(Boolean)
90
+ .join('')
91
+ }
92
+
93
+ function isCodexSystemStatus (text) {
94
+ return text.startsWith('== System Status ==\n') && text.includes('[automatic message added by system]')
95
+ }
96
+
45
97
  /**
46
98
  * Format a single prompt/response pair.
47
99
  */
@@ -130,4 +182,35 @@ function extractModel (filePath) {
130
182
  return null
131
183
  }
132
184
 
133
- module.exports = { parseTranscript, formatPair, formatBody, formatTitleTranscript, extractHeadline, fallbackHeadline, extractModel }
185
+ function extractCodexModel (filePath, hookModel) {
186
+ if (hookModel) return hookModel
187
+ if (!filePath || !fs.existsSync(filePath)) return null
188
+
189
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n')
190
+ for (const line of lines) {
191
+ if (!line.trim()) continue
192
+ let entry
193
+ try {
194
+ entry = JSON.parse(line)
195
+ } catch {
196
+ continue
197
+ }
198
+ if (entry.type === 'session_meta' && entry.payload?.model) return entry.payload.model
199
+ if (entry.type === 'session_meta' && entry.payload?.model_slug) return entry.payload.model_slug
200
+ }
201
+ return null
202
+ }
203
+
204
+ module.exports = {
205
+ parseTranscript,
206
+ parseCodexTranscript,
207
+ formatPair,
208
+ formatBody,
209
+ formatTitleTranscript,
210
+ extractHeadline,
211
+ fallbackHeadline,
212
+ extractModel,
213
+ extractCodexModel,
214
+ visibleText,
215
+ isCodexSystemStatus
216
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@searls/turbocommit",
3
- "version": "0.12.1",
4
- "description": "Auto-commit after every Claude Code turn",
3
+ "version": "0.13.0",
4
+ "description": "Auto-commit after every AI coding agent turn",
5
5
  "bin": {
6
6
  "turbocommit": "./cli.js"
7
7
  },
@@ -19,6 +19,9 @@
19
19
  "keywords": [
20
20
  "claude",
21
21
  "claude-code",
22
+ "codex",
23
+ "openai-codex",
24
+ "ai-agent",
22
25
  "git",
23
26
  "auto-commit"
24
27
  ],