@threadcrumbs/common 0.0.0-stage → 0.1.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/LICENSE.md +7 -0
- package/README.md +108 -3
- package/harnesses/README.md +213 -0
- package/harnesses/antigravity.mjs +269 -0
- package/harnesses/claude-code.mjs +578 -0
- package/harnesses/codex.mjs +419 -0
- package/harnesses/cursor.mjs +199 -0
- package/harnesses/hermes.mjs +211 -0
- package/harnesses/index.mjs +36 -0
- package/harnesses/kilocode.mjs +361 -0
- package/harnesses/opencode.mjs +449 -0
- package/launch/index.mjs +163 -0
- package/launch/macos.mjs +21 -0
- package/launch/registry.mjs +52 -0
- package/launch/terminal.mjs +213 -0
- package/launch/win-terminal.mjs +87 -0
- package/launch/windows.mjs +96 -0
- package/launch/xdg.mjs +44 -0
- package/lib/anthropic-prices.mjs +70 -0
- package/lib/claude-desktop.mjs +208 -0
- package/lib/claude-transcript.mjs +462 -0
- package/lib/cursor-folder.mjs +107 -0
- package/lib/fsutil.mjs +191 -0
- package/lib/hermes-home.mjs +30 -0
- package/lib/jsonl-follower.mjs +45 -0
- package/lib/opencode-store.mjs +602 -0
- package/lib/sqlite.mjs +29 -0
- package/lib/telemetry-fields.mjs +245 -0
- package/license/index.mjs +101 -0
- package/license/store.mjs +155 -0
- package/orgs/index.mjs +157 -0
- package/package.json +39 -4
- package/telemetry/README.md +148 -0
- package/telemetry/antigravity.mjs +268 -0
- package/telemetry/claude-code.mjs +370 -0
- package/telemetry/codex.mjs +556 -0
- package/telemetry/cursor.mjs +276 -0
- package/telemetry/hermes.mjs +404 -0
- package/telemetry/index.mjs +9 -0
- package/telemetry/kilocode.mjs +31 -0
- package/telemetry/opencode.mjs +36 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { claudeDesktopStores, desktopRecordReader, projectOf } from '../lib/claude-desktop.mjs'
|
|
5
|
+
import { awaitedPeers, readLiveSessions, transcriptDetail, transcriptReader, turnEnding } from '../lib/claude-transcript.mjs'
|
|
6
|
+
import { exists, listDirs, listFiles, num } from '../lib/fsutil.mjs'
|
|
7
|
+
import { DETAIL_LIMITS, RECENT_MS, number, planSummary, text, titleFromPrompt } from '../lib/telemetry-fields.mjs'
|
|
8
|
+
|
|
9
|
+
export { nextPlan } from '../lib/telemetry-fields.mjs'
|
|
10
|
+
|
|
11
|
+
const STALE_TURN_MS = 2 * 60 * 1000
|
|
12
|
+
const ACTIVE_WINDOW_MS = 30 * 60 * 1000
|
|
13
|
+
const SUBAGENT_WINDOW_MS = 10 * 60 * 1000
|
|
14
|
+
const OLDER_KEPT = 3
|
|
15
|
+
const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
16
|
+
|
|
17
|
+
export function claudeCode({
|
|
18
|
+
home = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'),
|
|
19
|
+
desktop = claudeDesktopStores(),
|
|
20
|
+
now = Date.now,
|
|
21
|
+
isAlive,
|
|
22
|
+
} = {}) {
|
|
23
|
+
const projectsDir = path.join(home, 'projects')
|
|
24
|
+
const liveDir = path.join(home, 'sessions')
|
|
25
|
+
const catalogDir = path.join(home, 'cache', 'model-catalog')
|
|
26
|
+
const transcripts = transcriptReader({ detail: true })
|
|
27
|
+
const subagentFiles = transcriptReader()
|
|
28
|
+
const subagentDetails = transcriptReader({ detail: true })
|
|
29
|
+
const desktopRecords = desktopRecordReader()
|
|
30
|
+
const olderReaders = new Map()
|
|
31
|
+
const metas = new Map()
|
|
32
|
+
let subagentsSweptAt = 0
|
|
33
|
+
let known = new Map()
|
|
34
|
+
let listed = new Map()
|
|
35
|
+
let catalog = { stamp: '', limits: new Map() }
|
|
36
|
+
|
|
37
|
+
async function contextLimits() {
|
|
38
|
+
const files = []
|
|
39
|
+
for (const file of await listFiles(catalogDir, (n) => n.endsWith('.json'))) {
|
|
40
|
+
const stat = await fsp.stat(file).catch(() => null)
|
|
41
|
+
if (stat) files.push({ file, mtime: stat.mtimeMs })
|
|
42
|
+
}
|
|
43
|
+
files.sort((a, b) => a.mtime - b.mtime)
|
|
44
|
+
const stamp = files.map((f) => `${f.file}@${f.mtime}`).join('|')
|
|
45
|
+
if (stamp === catalog.stamp) return catalog.limits
|
|
46
|
+
const limits = new Map()
|
|
47
|
+
for (const { file } of files) {
|
|
48
|
+
try {
|
|
49
|
+
const models = JSON.parse(await fsp.readFile(file, 'utf8'))?.catalog?.config?.models
|
|
50
|
+
for (const m of Array.isArray(models) ? models : []) {
|
|
51
|
+
const limit = number(m?.runtime?.max_input_tokens)
|
|
52
|
+
if (m?.id && limit) limits.set(m.id, limit)
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
catalog = { stamp, limits }
|
|
57
|
+
return limits
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function findTranscripts(live) {
|
|
61
|
+
const cutoff = now() - RECENT_MS
|
|
62
|
+
const byId = new Map()
|
|
63
|
+
for (const projectDir of await listDirs(projectsDir)) {
|
|
64
|
+
for (const file of await listFiles(projectDir, (n) => n.endsWith('.jsonl'))) {
|
|
65
|
+
const id = path.basename(file, '.jsonl')
|
|
66
|
+
const stat = await fsp.stat(file).catch(() => null)
|
|
67
|
+
if (!stat || (stat.mtimeMs < cutoff && !live.has(id))) continue
|
|
68
|
+
const existing = byId.get(id)
|
|
69
|
+
if (existing && existing.stat.mtimeMs >= stat.mtimeMs) continue
|
|
70
|
+
byId.set(id, { id, file, stat })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return [...byId.values()]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const subagentDir = (file, sessionId) => path.join(path.dirname(file), sessionId, 'subagents')
|
|
77
|
+
|
|
78
|
+
async function subagentTotals(file, sessionId) {
|
|
79
|
+
const cutoff = now() - SUBAGENT_WINDOW_MS
|
|
80
|
+
const totals = { running: 0, spend: 0, unpriced: false, added: 0, removed: 0 }
|
|
81
|
+
for (const sub of await listFiles(subagentDir(file, sessionId), (n) => n.endsWith('.jsonl'))) {
|
|
82
|
+
const stat = await fsp.stat(sub).catch(() => null)
|
|
83
|
+
if (!stat) continue
|
|
84
|
+
const state = await subagentFiles.read(sub, stat, { sidechain: true }).catch(() => null)
|
|
85
|
+
if (!state) continue
|
|
86
|
+
totals.spend += state.spend
|
|
87
|
+
totals.unpriced ||= state.unpriced
|
|
88
|
+
totals.added += state.lines.added
|
|
89
|
+
totals.removed += state.lines.removed
|
|
90
|
+
if (stat.mtimeMs >= cutoff && !turnEnding(state).handedBack) totals.running++
|
|
91
|
+
}
|
|
92
|
+
return totals
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sweepSubagents() {
|
|
96
|
+
if (now() - subagentsSweptAt < SUBAGENT_WINDOW_MS) return
|
|
97
|
+
subagentsSweptAt = now()
|
|
98
|
+
subagentFiles.sweep()
|
|
99
|
+
subagentDetails.sweep()
|
|
100
|
+
metas.clear()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readMeta(file) {
|
|
104
|
+
const stat = await fsp.stat(file).catch(() => null)
|
|
105
|
+
if (!stat) return null
|
|
106
|
+
const cached = metas.get(file)
|
|
107
|
+
if (cached?.mtime === stat.mtimeMs) return cached.meta
|
|
108
|
+
let meta = null
|
|
109
|
+
try {
|
|
110
|
+
meta = JSON.parse(await fsp.readFile(file, 'utf8'))
|
|
111
|
+
} catch {}
|
|
112
|
+
metas.set(file, { mtime: stat.mtimeMs, meta })
|
|
113
|
+
return meta
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function subagentList(file, sessionId) {
|
|
117
|
+
const found = []
|
|
118
|
+
for (const sub of await listFiles(subagentDir(file, sessionId), (n) => n.endsWith('.jsonl'))) {
|
|
119
|
+
const stat = await fsp.stat(sub).catch(() => null)
|
|
120
|
+
if (stat) found.push({ sub, stat })
|
|
121
|
+
}
|
|
122
|
+
found.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)
|
|
123
|
+
const list = []
|
|
124
|
+
for (const { sub, stat } of found.slice(0, DETAIL_LIMITS.subagents)) {
|
|
125
|
+
const state = await subagentFiles.read(sub, stat, { sidechain: true }).catch(() => null)
|
|
126
|
+
if (!state) continue
|
|
127
|
+
const meta = await readMeta(sub.replace(/\.jsonl$/, '.meta.json'))
|
|
128
|
+
const ending = turnEnding(state)
|
|
129
|
+
const lastActivityAt = Math.max(state.lastAt, Math.floor(stat.mtimeMs))
|
|
130
|
+
const open = !ending.handedBack && meta?.finished !== true
|
|
131
|
+
const status = ending.failed ? 'error' : !open ? 'done' : now() - lastActivityAt < SUBAGENT_WINDOW_MS ? 'working' : 'idle'
|
|
132
|
+
list.push({
|
|
133
|
+
id: path.basename(sub, '.jsonl').replace(/^agent-/, ''),
|
|
134
|
+
name: text(meta?.description) || titleFromPrompt(state.firstPrompt) || 'Subagent',
|
|
135
|
+
type: text(meta?.agentType),
|
|
136
|
+
status,
|
|
137
|
+
model: state.model,
|
|
138
|
+
tokens: { ...state.tokens },
|
|
139
|
+
tool: status === 'working' ? currentTool(state.open) : null,
|
|
140
|
+
startedAt: state.startedAt,
|
|
141
|
+
lastActivityAt,
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
return list.sort((a, b) => a.startedAt - b.startedAt)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function scanSessions() {
|
|
148
|
+
const [live, limits, marks] = await Promise.all([readLiveSessions(liveDir, isAlive), contextLimits(), desktopRecords(desktop).then(desktopMarks)])
|
|
149
|
+
const found = []
|
|
150
|
+
const files = new Map()
|
|
151
|
+
const states = new Map()
|
|
152
|
+
for (const { id, file, stat } of await findTranscripts(live)) {
|
|
153
|
+
let state
|
|
154
|
+
try {
|
|
155
|
+
state = await transcripts.read(file, stat)
|
|
156
|
+
} catch {
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
files.set(id, file)
|
|
160
|
+
states.set(id, state)
|
|
161
|
+
found.push({ id, file, stat, state })
|
|
162
|
+
}
|
|
163
|
+
const handedBackAt = (sessionId) => states.get(sessionId)?.handedBackAt || 0
|
|
164
|
+
const sessions = []
|
|
165
|
+
for (const { id, file, stat, state } of found) {
|
|
166
|
+
if (marks.get(id)?.archived) continue
|
|
167
|
+
const running = live.get(id)
|
|
168
|
+
if (!running && now() - (state.lastAt || stat.mtimeMs) > RECENT_MS) continue
|
|
169
|
+
const totals = await subagentTotals(file, id)
|
|
170
|
+
const awaiting = running ? awaitedPeers(state, live, handedBackAt) : []
|
|
171
|
+
sessions.push(toSession({ id, state, stat, running, subagents: running ? totals.running : 0, awaiting, totals, limits, mark: marks.get(id), now: now() }))
|
|
172
|
+
}
|
|
173
|
+
known = files
|
|
174
|
+
listed = new Map(sessions.map((s) => [s.id, s]))
|
|
175
|
+
transcripts.sweep()
|
|
176
|
+
sweepSubagents()
|
|
177
|
+
return sessions
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function subagentDetail(file, id, agentId) {
|
|
181
|
+
if (!/^[\w-]{1,100}$/.test(agentId)) return null
|
|
182
|
+
const sub = path.join(subagentDir(file, id), `agent-${agentId}.jsonl`)
|
|
183
|
+
const stat = await fsp.stat(sub).catch(() => null)
|
|
184
|
+
if (!stat) return null
|
|
185
|
+
const state = await subagentDetails.read(sub, stat, { sidechain: true })
|
|
186
|
+
return {
|
|
187
|
+
id: `claude-code:${id}`,
|
|
188
|
+
sessionId: id,
|
|
189
|
+
agentId,
|
|
190
|
+
source: sub,
|
|
191
|
+
...transcriptDetail(state),
|
|
192
|
+
subagents: null,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function locate(id) {
|
|
197
|
+
if (known.has(id)) return known.get(id)
|
|
198
|
+
if (!SESSION_ID.test(id)) return null
|
|
199
|
+
let newest = null
|
|
200
|
+
for (const projectDir of await listDirs(projectsDir)) {
|
|
201
|
+
const file = path.join(projectDir, `${id}.jsonl`)
|
|
202
|
+
const stat = await fsp.stat(file).catch(() => null)
|
|
203
|
+
if (stat && (!newest || stat.mtimeMs > newest.mtime)) newest = { file, mtime: stat.mtimeMs }
|
|
204
|
+
}
|
|
205
|
+
return newest?.file || null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function olderReader(file) {
|
|
209
|
+
const reader = olderReaders.get(file) || transcriptReader({ detail: true })
|
|
210
|
+
olderReaders.delete(file)
|
|
211
|
+
olderReaders.set(file, reader)
|
|
212
|
+
while (olderReaders.size > OLDER_KEPT) olderReaders.delete(olderReaders.keys().next().value)
|
|
213
|
+
return reader
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function readSession(sessionKey, { subagent = null } = {}) {
|
|
217
|
+
const id = String(sessionKey).replace(/^claude-code:/, '')
|
|
218
|
+
const file = await locate(id)
|
|
219
|
+
const stat = file ? await fsp.stat(file).catch(() => null) : null
|
|
220
|
+
if (!stat) return null
|
|
221
|
+
const state = await (known.get(id) === file ? transcripts : olderReader(file)).read(file, stat)
|
|
222
|
+
const [live, limits, marks] = await Promise.all([readLiveSessions(liveDir, isAlive), contextLimits(), desktopRecords(desktop).then(desktopMarks)])
|
|
223
|
+
if (marks.get(id)?.archived) return null
|
|
224
|
+
const running = live.get(id)
|
|
225
|
+
const totals = await subagentTotals(file, id)
|
|
226
|
+
const awaiting = running ? awaitedPeers(state, live) : []
|
|
227
|
+
const session =
|
|
228
|
+
listed.get(`claude-code:${id}`) ||
|
|
229
|
+
toSession({ id, state, stat, running, subagents: running ? totals.running : 0, awaiting, totals, limits, mark: marks.get(id), now: now() })
|
|
230
|
+
if (subagent != null) return { session, detail: await subagentDetail(file, id, String(subagent)) }
|
|
231
|
+
return {
|
|
232
|
+
session,
|
|
233
|
+
detail: { id: `claude-code:${id}`, sessionId: id, source: file, ...transcriptDetail(state), subagents: await subagentList(file, id) },
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function sessionDetail(sessionKey, { subagent = null } = {}) {
|
|
238
|
+
const id = String(sessionKey).replace(/^claude-code:/, '')
|
|
239
|
+
const file = known.get(id)
|
|
240
|
+
const stat = file ? await fsp.stat(file).catch(() => null) : null
|
|
241
|
+
if (!stat) return null
|
|
242
|
+
if (subagent != null) return subagentDetail(file, id, String(subagent))
|
|
243
|
+
const state = await transcripts.read(file, stat)
|
|
244
|
+
return {
|
|
245
|
+
id: `claude-code:${id}`,
|
|
246
|
+
sessionId: id,
|
|
247
|
+
source: file,
|
|
248
|
+
...transcriptDetail(state),
|
|
249
|
+
subagents: await subagentList(file, id),
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
id: 'claude-code',
|
|
255
|
+
name: 'Claude Code',
|
|
256
|
+
detect: () => exists(projectsDir),
|
|
257
|
+
watchRoots: () => [projectsDir, liveDir, ...desktop],
|
|
258
|
+
scanSessions,
|
|
259
|
+
sessionDetail,
|
|
260
|
+
readSession,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
function contextLimit(limits, model) {
|
|
266
|
+
if (!model) return 0
|
|
267
|
+
const bare = model.replace(/\[.*\]$/, '')
|
|
268
|
+
if (limits.has(bare)) return limits.get(bare)
|
|
269
|
+
for (const [id, limit] of limits) {
|
|
270
|
+
if (bare.startsWith(id) || id.startsWith(bare)) return limit
|
|
271
|
+
}
|
|
272
|
+
return 0
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function desktopMarks({ records, deleted }) {
|
|
276
|
+
const marks = new Map()
|
|
277
|
+
for (const r of records) {
|
|
278
|
+
if (typeof r.cliSessionId !== 'string' || !r.cliSessionId) continue
|
|
279
|
+
const next = {
|
|
280
|
+
archived: r.isArchived === true || r.isArchived === 'True',
|
|
281
|
+
originCwd: typeof r.originCwd === 'string' ? r.originCwd : '',
|
|
282
|
+
cwd: typeof r.cwd === 'string' ? r.cwd : '',
|
|
283
|
+
focusedAt: num(r.lastFocusedAt),
|
|
284
|
+
activeAt: num(r.lastActivityAt) || num(r.lastFocusedAt) || num(r.createdAt),
|
|
285
|
+
outcome: r.turnOutcome || '',
|
|
286
|
+
failed: Boolean(r.error) || r.turnOutcome === 'failed',
|
|
287
|
+
}
|
|
288
|
+
const seen = marks.get(r.cliSessionId)
|
|
289
|
+
marks.set(
|
|
290
|
+
r.cliSessionId,
|
|
291
|
+
seen
|
|
292
|
+
? {
|
|
293
|
+
archived: seen.archived && next.archived,
|
|
294
|
+
originCwd: seen.originCwd || next.originCwd,
|
|
295
|
+
cwd: seen.cwd || next.cwd,
|
|
296
|
+
focusedAt: Math.max(seen.focusedAt, next.focusedAt),
|
|
297
|
+
activeAt: Math.max(seen.activeAt, next.activeAt),
|
|
298
|
+
outcome: seen.outcome || next.outcome,
|
|
299
|
+
failed: seen.failed || next.failed,
|
|
300
|
+
}
|
|
301
|
+
: next
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
for (const id of deleted) if (!marks.has(id)) marks.set(id, { archived: true, outcome: '', failed: false })
|
|
305
|
+
return marks
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function statusOf({ running, state, subagents, awaiting, lastAt, mark, unread, now }) {
|
|
309
|
+
const midTurn = Boolean(state.ending && !state.ending.handedBack)
|
|
310
|
+
const { failed, onAgents: onlyAgents } = turnEnding(state)
|
|
311
|
+
const blocked = mark?.outcome === 'blocked'
|
|
312
|
+
const failedTurn = failed || Boolean(mark?.failed)
|
|
313
|
+
if (running) {
|
|
314
|
+
if (running.status === 'waiting') return 'asking'
|
|
315
|
+
const busy = running.status ? running.status === 'busy' : midTurn
|
|
316
|
+
if (busy) return onlyAgents ? 'delegating' : 'working'
|
|
317
|
+
if (subagents || awaiting.length) return 'delegating'
|
|
318
|
+
if (blocked) return 'asking'
|
|
319
|
+
if (failedTurn) return 'error'
|
|
320
|
+
return unread || now - lastAt < ACTIVE_WINDOW_MS ? 'done' : 'idle'
|
|
321
|
+
}
|
|
322
|
+
if (midTurn && now - lastAt < STALE_TURN_MS) return onlyAgents ? 'delegating' : 'working'
|
|
323
|
+
if (blocked) return 'asking'
|
|
324
|
+
if (failedTurn) return 'error'
|
|
325
|
+
return unread ? 'done' : 'idle'
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function currentTool(open) {
|
|
329
|
+
let latest = null
|
|
330
|
+
for (const call of open.values()) {
|
|
331
|
+
if (!latest || call.startedAt >= latest.startedAt) latest = call
|
|
332
|
+
}
|
|
333
|
+
return latest ? { name: latest.name, startedAt: latest.startedAt } : null
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function toSession({ id, state, stat, running, subagents, awaiting, totals, limits, mark, now }) {
|
|
337
|
+
const lastActivityAt = state.lastAt || Math.floor(stat.mtimeMs)
|
|
338
|
+
const unread = Boolean(mark) && (state.handedBackAt || mark.activeAt) > mark.focusedAt
|
|
339
|
+
const status = statusOf({ running, state, subagents, awaiting, lastAt: lastActivityAt, mark, unread, now })
|
|
340
|
+
const { projectPath, project, worktree } = projectOf(mark?.cwd || state.firstCwd || state.cwd, mark?.originCwd)
|
|
341
|
+
const limit = contextLimit(limits, state.model)
|
|
342
|
+
const promptTitle = state.firstPrompt ? titleFromPrompt(state.firstPrompt) : ''
|
|
343
|
+
return {
|
|
344
|
+
id: `claude-code:${id}`,
|
|
345
|
+
harness: 'claude-code',
|
|
346
|
+
title: state.customTitle || state.aiTitle || running?.name || state.summary || promptTitle || 'Untitled session',
|
|
347
|
+
project,
|
|
348
|
+
projectPath,
|
|
349
|
+
worktree,
|
|
350
|
+
cwd: state.cwd,
|
|
351
|
+
gitBranch: state.gitBranch,
|
|
352
|
+
model: state.model,
|
|
353
|
+
effort: state.effort,
|
|
354
|
+
status,
|
|
355
|
+
live: Boolean(running),
|
|
356
|
+
startedAt: state.startedAt,
|
|
357
|
+
lastActivityAt,
|
|
358
|
+
tool: status === 'working' || status === 'delegating' ? currentTool(state.open) : null,
|
|
359
|
+
tokens: { ...state.tokens },
|
|
360
|
+
context: state.context ? { used: state.context, limit } : null,
|
|
361
|
+
costUsd: state.unpriced || totals.unpriced ? (state.cost ? state.cost.usd : null) : state.spend + totals.spend,
|
|
362
|
+
lines: { added: state.lines.added + totals.added, removed: state.lines.removed + totals.removed },
|
|
363
|
+
plan: planSummary(state.plan),
|
|
364
|
+
subagents,
|
|
365
|
+
awaiting: awaiting.map((p) => ({ id: `claude-code:${p.sessionId}`, title: p.name })),
|
|
366
|
+
prompts: state.prompts,
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export default claudeCode()
|