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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kira-glasses-bridge",
3
- "version": "2.35.0",
3
+ "version": "2.36.0",
4
4
  "description": "Connects your Mac to Kira, the assistant for Even G2 glasses: Claude Code sessions, files, folders and apps, by voice.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- function textOf(message) {
12
- const c = message?.content
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
- const turns = []
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 turns = await turnsOf(s.sessionId, cwd)
63
- const lastUser = [...turns].reverse().find(t => t.role === 'user')
64
- const lastClaude = [...turns].reverse().find(t => t.role === 'claude')
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: activeOf(s),
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
- export async function readSession(sessionId, turns = 6) {
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 all = await turnsOf(sessionId, cwd)
87
+ const items = await itemsFor(sessionId, cwd)
87
88
  const n = Math.min(Math.max(Number(turns) || 6, 1), 30)
88
- return { session_id: info.sessionId, title: titleOf(info), cwd, active: activeOf(info), last_active_at: new Date(info.lastModified).toISOString(), turns: all.slice(-n).map(t => ({ ...t, text: t.text.slice(0, 2000) })) }
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
+ }
@@ -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
  }