kira-glasses-bridge 2.35.0 → 2.36.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/package.json +1 -1
- package/src/claude-sessions.mjs +36 -24
- package/src/claude-transcript.mjs +114 -0
- package/src/session-tools.mjs +1 -1
package/package.json
CHANGED
package/src/claude-sessions.mjs
CHANGED
|
@@ -5,29 +5,22 @@ import { getSessionInfo, getSessionMessages, listSessions as sdkListSessions } f
|
|
|
5
5
|
import { open, readdir, stat } from 'node:fs/promises'
|
|
6
6
|
import { homedir } from 'node:os'
|
|
7
7
|
import { basename, join } from 'node:path'
|
|
8
|
+
import { doingOf, itemsOf, lastTurns, turnsOf } from './claude-transcript.mjs'
|
|
8
9
|
|
|
9
10
|
const ACTIVE_WINDOW_MS = 3 * 60 * 1000
|
|
11
|
+
const TEXT_CAP = 2000
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (typeof c === 'string') return c
|
|
14
|
-
if (Array.isArray(c)) return c.filter(b => b?.type === 'text').map(b => b.text).join('\n')
|
|
15
|
-
return ''
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** The conversation as said: user and Claude text, without tool results, system blocks or subagents. */
|
|
19
|
-
async function turnsOf(sessionId, dir) {
|
|
13
|
+
/** The session as it happened: its turns and the tool calls between them (claude-transcript.mjs). */
|
|
14
|
+
async function itemsFor(sessionId, dir) {
|
|
20
15
|
const messages = await getSessionMessages(sessionId, dir ? { dir } : undefined)
|
|
21
|
-
|
|
22
|
-
for (const m of messages) {
|
|
23
|
-
if (m.parent_tool_use_id) continue
|
|
24
|
-
const text = textOf(m.message).trim()
|
|
25
|
-
if (!text || text.startsWith('<')) continue // tool results / system blocks
|
|
26
|
-
turns.push({ role: m.type === 'user' ? 'user' : 'claude', text, at: m.timestamp ?? null })
|
|
27
|
-
}
|
|
28
|
-
return turns
|
|
16
|
+
return itemsOf(messages, dir ?? homedir())
|
|
29
17
|
}
|
|
30
18
|
|
|
19
|
+
/** The branch, unless the checkout is detached ("HEAD"). */
|
|
20
|
+
const branchOf = s => (s.gitBranch && s.gitBranch !== 'HEAD' ? s.gitBranch : null)
|
|
21
|
+
|
|
22
|
+
const capText = item => (item.kind === 'turn' ? { ...item, text: item.text.slice(0, TEXT_CAP) } : item)
|
|
23
|
+
|
|
31
24
|
/**
|
|
32
25
|
* The SDK reads a session's folder from the start of its transcript, and misses it when the
|
|
33
26
|
* transcript opens with large snapshot entries. Then: the first "cwd" in the first MB of the file.
|
|
@@ -59,18 +52,22 @@ export async function listSessions(max = 10) {
|
|
|
59
52
|
for (const s of await sdkListSessions({ limit: n })) {
|
|
60
53
|
try {
|
|
61
54
|
const cwd = await cwdOf(s)
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
55
|
+
const items = await itemsFor(s.sessionId, cwd)
|
|
56
|
+
const turns = turnsOf(items)
|
|
57
|
+
const lastUser = turns.findLast(t => t.role === 'user')
|
|
58
|
+
const lastClaude = turns.findLast(t => t.role === 'claude')
|
|
59
|
+
const active = activeOf(s)
|
|
65
60
|
sessions.push({
|
|
66
61
|
session_id: s.sessionId,
|
|
67
62
|
title: titleOf(s) ?? (lastUser ? lastUser.text.slice(0, 60) : '(untitled)'),
|
|
68
63
|
project: cwd ? cwd.split('/').slice(-2).join('/') : null,
|
|
69
64
|
cwd,
|
|
70
|
-
active
|
|
65
|
+
active,
|
|
71
66
|
last_active_at: new Date(s.lastModified).toISOString(),
|
|
72
67
|
last_user_message: lastUser ? lastUser.text.slice(0, 200) : null,
|
|
73
68
|
last_claude_message: lastClaude ? lastClaude.text.slice(0, 200) : null,
|
|
69
|
+
branch: branchOf(s),
|
|
70
|
+
doing: active ? doingOf(items) : null,
|
|
74
71
|
})
|
|
75
72
|
} catch {
|
|
76
73
|
/* unreadable transcript */
|
|
@@ -79,13 +76,28 @@ export async function listSessions(max = 10) {
|
|
|
79
76
|
return { count: sessions.length, sessions }
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
|
|
79
|
+
/**
|
|
80
|
+
* A session's last `turns` turns; with `items`, the tool calls between them too (the phone asks for
|
|
81
|
+
* them; Kira's own reads don't, to keep them short).
|
|
82
|
+
*/
|
|
83
|
+
export async function readSession(sessionId, turns = 6, { items: withItems = false } = {}) {
|
|
83
84
|
const info = await getSessionInfo(sessionId).catch(() => undefined)
|
|
84
85
|
if (!info) return { error: `No session ${sessionId} on this machine` }
|
|
85
86
|
const cwd = await cwdOf(info)
|
|
86
|
-
const
|
|
87
|
+
const items = await itemsFor(sessionId, cwd)
|
|
87
88
|
const n = Math.min(Math.max(Number(turns) || 6, 1), 30)
|
|
88
|
-
|
|
89
|
+
const recent = lastTurns(items, n).map(capText)
|
|
90
|
+
return {
|
|
91
|
+
session_id: info.sessionId,
|
|
92
|
+
title: titleOf(info),
|
|
93
|
+
cwd,
|
|
94
|
+
active: activeOf(info),
|
|
95
|
+
last_active_at: new Date(info.lastModified).toISOString(),
|
|
96
|
+
turns: turnsOf(recent),
|
|
97
|
+
...(withItems ? { items: recent } : {}),
|
|
98
|
+
branch: branchOf(info),
|
|
99
|
+
doing: activeOf(info) ? doingOf(items) : null,
|
|
100
|
+
}
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
/** Project folders Claude Code has been used in (that still exist), newest first. */
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// A Claude Code transcript (the Agent SDK's session messages) as the conversation shows it: what was
|
|
2
|
+
// said and, between the turns, the tool calls Claude made, described the way its jobs describe them
|
|
3
|
+
// (agents/claude-calls.mjs). The shape is @kira/genui's claudeSessionItems.ts.
|
|
4
|
+
import { describeAction, short } from './agents/claude-calls.mjs'
|
|
5
|
+
|
|
6
|
+
/** How much of an edit, a write or a command a call carries for its detail. */
|
|
7
|
+
const DETAIL_CAP = 1500
|
|
8
|
+
|
|
9
|
+
function textOf(message) {
|
|
10
|
+
const c = message?.content
|
|
11
|
+
if (typeof c === 'string') return c
|
|
12
|
+
if (Array.isArray(c)) return c.filter(b => b?.type === 'text').map(b => b.text).join('\n')
|
|
13
|
+
return ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** An edit's lines: the old and new text without the lines they share at the start and the end. */
|
|
17
|
+
function diffCounts(old, now) {
|
|
18
|
+
const a = String(old ?? '').split('\n')
|
|
19
|
+
const b = String(now ?? '').split('\n')
|
|
20
|
+
let start = 0
|
|
21
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++
|
|
22
|
+
let end = 0
|
|
23
|
+
while (end < a.length - start && end < b.length - start && a[a.length - 1 - end] === b[b.length - 1 - end]) end++
|
|
24
|
+
return { adds: b.length - start - end, dels: a.length - start - end }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Lines an edit or a write adds and takes out; null for any other call. */
|
|
28
|
+
export function lineCounts(name, input) {
|
|
29
|
+
const i = input ?? {}
|
|
30
|
+
if (name === 'Write') return { adds: i.content ? String(i.content).split('\n').length : 0, dels: 0 }
|
|
31
|
+
if (name === 'Edit') return diffCounts(i.old_string, i.new_string)
|
|
32
|
+
if (name !== 'MultiEdit') return null
|
|
33
|
+
const each = (i.edits ?? []).map(e => diffCounts(e.old_string, e.new_string))
|
|
34
|
+
return { adds: each.reduce((n, c) => n + c.adds, 0), dels: each.reduce((n, c) => n + c.dels, 0) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A change's detail, shorter than a job's: the conversation polls it every few seconds. A command
|
|
39
|
+
* has none (its target is the command).
|
|
40
|
+
*/
|
|
41
|
+
function shortDetail(d) {
|
|
42
|
+
const cut = e => ({ ...e, old: short(e.old, DETAIL_CAP), new: short(e.new, DETAIL_CAP) })
|
|
43
|
+
if (d.edit) return { edit: cut(d.edit) }
|
|
44
|
+
if (d.edits) return { edits: d.edits.slice(0, 5).map(cut) }
|
|
45
|
+
return d.content !== undefined ? { content: short(d.content, DETAIL_CAP) } : undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One tool_use block as a call. */
|
|
49
|
+
function toolCall(block, cwd) {
|
|
50
|
+
const act = describeAction(block.name, block.input, cwd)
|
|
51
|
+
const detail = act.detail ? shortDetail(act.detail) : undefined
|
|
52
|
+
return {
|
|
53
|
+
id: block.id,
|
|
54
|
+
type: act.type,
|
|
55
|
+
label: act.label,
|
|
56
|
+
target: act.target,
|
|
57
|
+
...(lineCounts(block.name, block.input) ?? {}),
|
|
58
|
+
...(detail ? { detail } : {}),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Everything said and done, in order: the user's and Claude's turns, and each run of tool calls
|
|
64
|
+
* between them as one item. Subagents' own messages and system blocks (text starting "<") are left out.
|
|
65
|
+
*/
|
|
66
|
+
export function itemsOf(messages, cwd) {
|
|
67
|
+
const items = []
|
|
68
|
+
const calls = new Map()
|
|
69
|
+
let group = null
|
|
70
|
+
const say = (role, text, at) => {
|
|
71
|
+
group = null
|
|
72
|
+
if (text && !text.startsWith('<')) items.push({ kind: 'turn', role, text, at })
|
|
73
|
+
}
|
|
74
|
+
for (const m of messages) {
|
|
75
|
+
if (m.parent_tool_use_id) continue
|
|
76
|
+
const at = m.timestamp ?? null
|
|
77
|
+
const content = Array.isArray(m.message?.content) ? m.message.content : []
|
|
78
|
+
if (m.type === 'assistant') {
|
|
79
|
+
for (const b of content) {
|
|
80
|
+
if (b?.type === 'text') say('claude', String(b.text ?? '').trim(), at)
|
|
81
|
+
if (b?.type !== 'tool_use') continue
|
|
82
|
+
if (!group) items.push((group = { kind: 'tools', at, calls: [] }))
|
|
83
|
+
const call = toolCall(b, cwd)
|
|
84
|
+
calls.set(call.id, call)
|
|
85
|
+
group.calls.push(call)
|
|
86
|
+
}
|
|
87
|
+
} else if (m.type === 'user') {
|
|
88
|
+
for (const b of content) {
|
|
89
|
+
if (b?.type === 'tool_result' && b.is_error && calls.has(b.tool_use_id)) calls.get(b.tool_use_id).failed = true
|
|
90
|
+
}
|
|
91
|
+
const text = textOf(m.message).trim()
|
|
92
|
+
if (text) say('user', text, at)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return items
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Just the turns (what read_claude_session has always returned). */
|
|
99
|
+
export const turnsOf = items => items.filter(i => i.kind === 'turn').map(({ kind, ...turn }) => turn)
|
|
100
|
+
|
|
101
|
+
/** The last `n` turns and the tool calls among and after them. */
|
|
102
|
+
export function lastTurns(items, n) {
|
|
103
|
+
let seen = 0
|
|
104
|
+
for (let i = items.length - 1; i >= 0; i--) if (items[i].kind === 'turn' && ++seen === n) return items.slice(i)
|
|
105
|
+
return items
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The last call when nothing was said after it: what the session is doing (or did last). */
|
|
109
|
+
export function doingOf(items) {
|
|
110
|
+
const last = items.at(-1)
|
|
111
|
+
if (last?.kind !== 'tools') return null
|
|
112
|
+
const { detail, ...call } = last.calls.at(-1)
|
|
113
|
+
return call
|
|
114
|
+
}
|
package/src/session-tools.mjs
CHANGED
|
@@ -15,6 +15,6 @@ export const sessionTools = {
|
|
|
15
15
|
return listSessions(args.max)
|
|
16
16
|
},
|
|
17
17
|
async read_claude_session(args) {
|
|
18
|
-
return readSession(String(args.session_id ?? ''), args.turns)
|
|
18
|
+
return readSession(String(args.session_id ?? ''), args.turns, { items: args.items === true })
|
|
19
19
|
},
|
|
20
20
|
}
|