@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/README.md +61 -22
- package/cli.js +65 -42
- package/lib/agent.js +11 -4
- package/lib/codex.js +34 -0
- package/lib/config.js +52 -0
- package/lib/doctor.js +102 -10
- package/lib/harness.js +99 -0
- package/lib/init.js +18 -12
- package/lib/install.js +123 -5
- package/lib/log.js +5 -3
- package/lib/monitor.js +16 -3
- package/lib/run.js +105 -40
- package/lib/session.js +27 -21
- package/lib/track.js +14 -11
- package/lib/transcript.js +84 -1
- package/package.json +5 -2
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
const hookInput = typeof input === 'string' ? parseInput(input) : input
|
|
45
|
+
root = root || hookInput?.root
|
|
46
|
+
if (!root || !hookInput) return
|
|
45
47
|
|
|
46
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Auto-commit after every
|
|
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
|
],
|