kira-glasses-bridge 2.32.0 → 2.34.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.32.0",
3
+ "version": "2.34.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": {
@@ -58,6 +58,7 @@ export function describeAction(name, input, cwd) {
58
58
  return act('fetch', 'Fetch', short(i.url, 200))
59
59
  case 'WebSearch':
60
60
  return act('fetch', 'Search the web', short(i.query, 160))
61
+ case 'Agent':
61
62
  case 'Task':
62
63
  return act('delegate', 'Subtask', short(i.description ?? i.prompt, 160))
63
64
  case 'TodoWrite':
@@ -15,6 +15,8 @@ import { planLimits } from './claude-local.mjs'
15
15
  import { contextFrom, limitsFromEvent, rejectedWindow, stoppedBy } from './claude-usage.mjs'
16
16
  import { sdkOptionsFor } from './settings.mjs'
17
17
  import { kiraPluginOptions } from '../kira-plugin.mjs'
18
+ import { git } from '../git.mjs'
19
+ import { isAgentCall, subagentFeed } from './subagents.mjs'
18
20
 
19
21
  /** Kira's MCP server as Claude Code connects to it: Streamable HTTP, the token as the bearer. */
20
22
  const kiraServer = ({ url, token }) => ({ type: 'http', url, headers: { Authorization: `Bearer ${token}` } })
@@ -100,6 +102,9 @@ async function run({ prompt, sessionId, cwd, persona, settings, abort }, io) {
100
102
  const env = { ...process.env, CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: '1', ENABLE_CLAUDEAI_MCP_SERVERS: 'false' }
101
103
  /** Background tasks the model still managed to start (an older CLI ignoring the flag): tool_use id -> description. */
102
104
  const orphans = new Map()
105
+ /** The agents it starts, each with where it works (subagents.mjs). */
106
+ const branchOf = async path => (await git(path, ['branch', '--show-current']))?.trim() || null
107
+ const agents = subagentFeed(e => io.emit(e), branchOf)
103
108
 
104
109
  try {
105
110
  const q = query({
@@ -144,6 +149,9 @@ async function run({ prompt, sessionId, cwd, persona, settings, abort }, io) {
144
149
  if (m.is_backgrounded && m.tool_use_id) orphans.set(m.tool_use_id, m.description ?? m.subagent_type ?? 'task')
145
150
  } else if (m.type === 'system' && m.subtype === 'task_notification') {
146
151
  if (m.tool_use_id) orphans.delete(m.tool_use_id)
152
+ if (m.tool_use_id) agents.ended(m.tool_use_id, m.status, m.summary)
153
+ } else if (m.type === 'system' && m.subtype === 'task_progress') {
154
+ if (m.tool_use_id) agents.progress(m.tool_use_id, m.summary)
147
155
  } else if (m.type === 'assistant') {
148
156
  // A new turn of the main thread: how full the context is now, as /context counts it (no model call).
149
157
  if (!nested && m.message?.id && m.message.id !== turn) {
@@ -153,7 +161,13 @@ async function run({ prompt, sessionId, cwd, persona, settings, abort }, io) {
153
161
  for (const b of m.message?.content ?? []) {
154
162
  // A subagent's narration is for its parent, not the user; its actions still show what is going on while the Subtask runs.
155
163
  if (b.type === 'text' && b.text?.trim() && !nested) io.emit({ kind: 'message', text: short(b.text, TEXT_CAP) })
156
- else if (b.type === 'tool_use') io.emit({ kind: 'action', id: b.id, tool: b.name, ...describeAction(b.name, b.input, cwd) })
164
+ else if (b.type === 'tool_use') {
165
+ // A step inside an agent says whose it is.
166
+ const whose = nested ? { agent: m.parent_tool_use_id } : {}
167
+ io.emit({ kind: 'action', id: b.id, tool: b.name, ...describeAction(b.name, b.input, cwd), ...whose })
168
+ if (isAgentCall(b.name)) agents.called(b.id, b.input, m.parent_tool_use_id)
169
+ if (nested) agents.touched(m.parent_tool_use_id, b.input)
170
+ }
157
171
  }
158
172
  } else if (m.type === 'user') {
159
173
  for (const b of m.message?.content ?? []) {
@@ -161,10 +175,12 @@ async function run({ prompt, sessionId, cwd, persona, settings, abort }, io) {
161
175
  const t = typeof b.content === 'string' ? b.content : Array.isArray(b.content) ? b.content.map(c => (c.type === 'text' ? c.text : '')).join('\n') : ''
162
176
  if (/^Async agent launched successfully/.test(t.trim()) && !orphans.has(b.tool_use_id)) orphans.set(b.tool_use_id, 'subagent')
163
177
  io.emit({ kind: 'action_result', id: b.tool_use_id, ok: !b.is_error, preview: short(t.trim(), 240) })
178
+ if (agents.has(b.tool_use_id)) agents.returned(b.tool_use_id, m.tool_use_result, b.is_error)
164
179
  }
165
180
  }
166
181
  } else if (m.type === 'result') {
167
182
  session = m.session_id ?? session
183
+ agents.stopAll()
168
184
  // A turn that ended on an API error (a usage limit among them) is a success carrying the error.
169
185
  const limit = { limits: usage.known, rejected, limitPrefixes: USAGE_LIMIT_ERROR_PREFIXES }
170
186
  if (m.subtype === 'success' && !m.is_error) text = String(m.result ?? '')
@@ -79,6 +79,8 @@ export function itemAction(item, cwd) {
79
79
  case 'plan':
80
80
  return act('plan', 'Plan', short(item.text, 160), { tool: 'Plan' })
81
81
  case 'collabAgentToolCall':
82
+ // Spawning one is a step; waiting on or messaging its agents moves the agent's row (codex-subagents.mjs).
83
+ if (item.tool !== 'spawnAgent') return null
82
84
  return act('delegate', 'Subtask', short(item.prompt ?? '', 160), { tool: 'Subtask' })
83
85
  case 'enteredReviewMode':
84
86
  return act('read', 'Review', short(item.review ?? '', 160), { tool: 'Review' })
@@ -0,0 +1,60 @@
1
+ // The agents Codex spawns inside a turn (its collab tools), as protocol `agent` events, the way
2
+ // subagents.mjs does Claude Code's. A spawn is a collabAgentToolCall naming the new agent's thread;
3
+ // that thread's items then arrive on the same app server, so each one says whose it is (`whose`), and
4
+ // later collab calls and subAgentActivity items say how each agent stands. Codex's agents work in the
5
+ // turn's own folder.
6
+ import { short } from './claude-calls.mjs'
7
+
8
+ const STATES = {
9
+ completed: 'done',
10
+ errored: 'failed',
11
+ interrupted: 'stopped',
12
+ shutdown: 'stopped',
13
+ notFound: 'stopped',
14
+ }
15
+ const ACTIVITY = { completed: 'done', interrupted: 'stopped' }
16
+
17
+ /** The last part of an agent's path ("/root/explorer" → "explorer"): the name Codex gave it. */
18
+ const nameOf = path => String(path ?? '').split('/').filter(Boolean).pop() || null
19
+
20
+ export function codexAgentFeed(emit) {
21
+ const agents = new Map()
22
+ /** Each agent's thread, to the spawn call that made it. */
23
+ const threads = new Map()
24
+ const send = a => {
25
+ agents.set(a.id, a)
26
+ emit(a)
27
+ }
28
+ const settle = (threadId, status, name) => {
29
+ const a = agents.get(threads.get(threadId))
30
+ if (!a) return
31
+ const named = name && !a.name ? { name } : {}
32
+ if (status && a.status === 'running') send({ ...a, status, ...named })
33
+ else if (named.name) send({ ...a, ...named })
34
+ }
35
+ return {
36
+ /** The agent a thread's items belong to, if the thread is one Codex spawned. */
37
+ whose: threadId => threads.get(threadId) ?? null,
38
+ started(item) {
39
+ if (item.type !== 'collabAgentToolCall' || item.tool !== 'spawnAgent') return
40
+ const description = short(item.prompt ?? '', 200)
41
+ send({ kind: 'agent', id: item.id, type: 'agent', description, status: 'running' })
42
+ },
43
+ completed(item) {
44
+ if (item.type === 'subAgentActivity') {
45
+ return settle(item.agentThreadId, ACTIVITY[item.kind], nameOf(item.agentPath))
46
+ }
47
+ if (item.type !== 'collabAgentToolCall') return
48
+ if (item.tool === 'spawnAgent') {
49
+ for (const t of item.receiverThreadIds ?? []) threads.set(t, item.id)
50
+ const a = agents.get(item.id)
51
+ if (a && item.status === 'failed') send({ ...a, status: 'failed' })
52
+ }
53
+ for (const [t, state] of Object.entries(item.agentsStates ?? {})) settle(t, STATES[state?.status])
54
+ },
55
+ /** The turn ended: what still runs ended with it (`status`: done, or stopped when it was cut short). */
56
+ endAll(status) {
57
+ for (const a of agents.values()) if (a.status === 'running') send({ ...a, status })
58
+ },
59
+ }
60
+ }
@@ -9,6 +9,7 @@ import { codexBin, openCodex } from './codex-rpc.mjs'
9
9
  import { codexInput } from './codex-input.mjs'
10
10
  import { turnSteering } from './codex-steer.mjs'
11
11
  import { codexStream } from './codex-live.mjs'
12
+ import { codexAgentFeed } from './codex-subagents.mjs'
12
13
  import { liveEmitter } from './live.mjs'
13
14
  import { kiraDecides } from './checks.mjs'
14
15
  import { CODEX_ASKS, askPerson, refusedByPolicy, said } from './codex-ask.mjs'
@@ -51,6 +52,8 @@ async function run({ prompt, attachments, sessionId, cwd, persona, settings, abo
51
52
  /** What is still being written (live.mjs): sent as it comes, the final form of each kept. */
52
53
  const live = liveEmitter(io.emit)
53
54
  const stream = codexStream()
55
+ /** The agents it spawns, and which thread is whose (codex-subagents.mjs). */
56
+ const agents = codexAgentFeed(e => io.emit(e))
54
57
  let finished
55
58
  const ended = new Promise(resolve => (finished = resolve))
56
59
  // What the person says while a turn runs goes into it (`codex` is the app server, opened below).
@@ -86,16 +89,22 @@ async function run({ prompt, attachments, sessionId, cwd, persona, settings, abo
86
89
  }
87
90
 
88
91
  const onNotify = (method, p) => {
92
+ // An item from a thread Codex spawned is its agent's: its steps say so, and what it says is for Codex.
93
+ const whose = p?.threadId ? agents.whose(p.threadId) : null
89
94
  const update = stream.update(method, p)
90
- if (update) return live.update(update)
95
+ if (update) return whose ? undefined : live.update(update)
91
96
  if (method === 'item/started') {
92
97
  if (p.item.type === 'fileChange') changes.set(p.item.id, p.item.changes ?? [])
98
+ agents.started(p.item)
93
99
  const action = itemAction(p.item, cwd)
94
- if (action) io.emit({ kind: 'action', id: p.item.id, ...action })
100
+ if (action) io.emit({ kind: 'action', id: p.item.id, ...action, ...(whose ? { agent: whose } : {}) })
95
101
  } else if (method === 'item/completed') {
102
+ agents.completed(p.item)
96
103
  const reviewed = review.completed(p.item)
97
104
  if (reviewed) io.emit(reviewed)
98
- if (p.item.type === 'agentMessage' && p.item.text?.trim()) {
105
+ if (whose && p.item.type === 'agentMessage') {
106
+ // Its agent's answer goes to Codex, not the person.
107
+ } else if (p.item.type === 'agentMessage' && p.item.text?.trim()) {
99
108
  text = p.item.text
100
109
  if (!review.repeats(text)) live.final({ kind: 'message', id: p.item.id, text: short(p.item.text, TEXT_CAP) })
101
110
  } else if (itemAction(p.item, cwd)) {
@@ -117,6 +126,7 @@ async function run({ prompt, attachments, sessionId, cwd, persona, settings, abo
117
126
  steering.ended()
118
127
  if (t.status === 'failed') error = usage.stopped(stoppedBy(t.error ?? { message: 'failed' }, usage.limits))
119
128
  if (t.status === 'interrupted') error = 'Stopped by the user.'
129
+ agents.endAll(t.status === 'completed' ? 'done' : 'stopped')
120
130
  finished({ durationMs: t.durationMs ?? undefined })
121
131
  }
122
132
  }
@@ -10,7 +10,13 @@
10
10
  // action { kind, id, type, tool, label, target, note?, detail? }
11
11
  // type: read | search | edit | write | run | fetch | delegate | plan | other
12
12
  // detail: { edit: {old,new,all} | edits: [...] | content | command }
13
+ // agent: the id of the agent (below) whose step it is, when an agent the turn started took it
13
14
  // action_result { kind, id, ok, preview? }
15
+ // agent { kind, id, type, description, status, name?, isolated?, background?, summary?,
16
+ // worktree?: { path, branch? }, stats?: { tools?, ms?, adds?, dels? }, agent? }
17
+ // an agent the turn started (Claude Code's Agent tool, Codex's spawnAgent), sent again whole
18
+ // each time it changes; status: running | done | failed | stopped; worktree: its own, when it
19
+ // has one (subagents.mjs, codex-subagents.mjs)
14
20
  // request { kind, request: AgentRequest } needs a person; the job waits
15
21
  // request_resolved { kind, id, answer, by } by: voice | lens | phone | portal | timeout | policy
16
22
  // usage { kind, context: {used,window} | null, limits: AgentLimits | null } how full the thread
@@ -0,0 +1,121 @@
1
+ // The agents Claude Code starts inside a turn (its Agent tool, once called Task), as protocol `agent`
2
+ // events: what kind each is, what it was asked, how it's going, and where it works: the job's folder,
3
+ // or a worktree of its own (`isolation: "worktree"`, under <repo>/.claude/worktrees/<name>). Its own
4
+ // steps carry its id as `agent` (claude.mjs); the worktree shows up in the first path one of them
5
+ // touches, and the finished call names it too. Each change of an agent is the whole event again, so
6
+ // the last one with an id is where it stands.
7
+ import { display } from '../git.mjs'
8
+
9
+ /** Claude Code's tool for starting an agent, by its current name and its old one. */
10
+ export const isAgentCall = name => name === 'Agent' || name === 'Task'
11
+
12
+ const WORKTREE = /^(.*\/\.claude\/worktrees\/[^/]+)(?:\/|$)/
13
+
14
+ /** The agent worktree a path is inside, if any. */
15
+ export function worktreeOf(path) {
16
+ const m = WORKTREE.exec(String(path ?? ''))
17
+ return m ? m[1] : null
18
+ }
19
+
20
+ /** Every absolute path a tool call names, for finding where an agent works. */
21
+ export function pathsIn(input) {
22
+ const i = input ?? {}
23
+ const named = [i.file_path, i.notebook_path, i.path].filter(p => typeof p === 'string')
24
+ const cd = /(?:^|&&|;)\s*cd\s+("[^"]+"|'[^']+'|\S+)/.exec(String(i.command ?? ''))
25
+ if (cd) named.push(cd[1].replace(/^["']|["']$/g, ''))
26
+ return named.filter(p => p.startsWith('/'))
27
+ }
28
+
29
+ /** An Agent call as the agent it starts. */
30
+ export function agentFrom(id, input, parent) {
31
+ const i = input ?? {}
32
+ return {
33
+ kind: 'agent',
34
+ id,
35
+ type: String(i.subagent_type || 'general-purpose'),
36
+ description: String(i.description || i.prompt || '').slice(0, 200),
37
+ status: 'running',
38
+ ...(i.name ? { name: String(i.name) } : {}),
39
+ ...(i.isolation === 'worktree' ? { isolated: true } : {}),
40
+ ...(parent ? { agent: parent } : {}),
41
+ }
42
+ }
43
+
44
+ /** A finished Agent call's result (the SDK's tool_use_result): how it went, where, and what it did. */
45
+ export function finished(agent, result, failed) {
46
+ const r = result && typeof result === 'object' ? result : {}
47
+ if (r.status === 'async_launched') return { ...agent, background: true }
48
+ const s = r.toolStats
49
+ const stats = {
50
+ ...(typeof r.totalToolUseCount === 'number' ? { tools: r.totalToolUseCount } : {}),
51
+ ...(typeof r.totalDurationMs === 'number' ? { ms: r.totalDurationMs } : {}),
52
+ ...(s ? { adds: s.linesAdded ?? 0, dels: s.linesRemoved ?? 0 } : {}),
53
+ }
54
+ // Claude Code removes an agent's worktree when it changed nothing, and then doesn't name it.
55
+ const removed = r.status === 'completed' && agent.isolated
56
+ const worktree = r.worktreePath
57
+ ? { path: display(r.worktreePath), ...(r.worktreeBranch ? { branch: r.worktreeBranch } : {}) }
58
+ : removed
59
+ ? null
60
+ : agent.worktree
61
+ const { worktree: _, ...rest } = agent
62
+ return {
63
+ ...rest,
64
+ status: failed ? 'failed' : 'done',
65
+ ...(worktree ? { worktree } : {}),
66
+ ...(Object.keys(stats).length ? { stats } : {}),
67
+ }
68
+ }
69
+
70
+ const ENDED = { completed: 'done', failed: 'failed', stopped: 'stopped' }
71
+
72
+ /**
73
+ * A turn's agents as they go. `emit` sends each change; `branchOf(path)` reads a worktree's branch
74
+ * (git, or a stand-in in tests).
75
+ */
76
+ export function subagentFeed(emit, branchOf) {
77
+ const agents = new Map()
78
+ const send = (a, more = {}) => {
79
+ agents.set(a.id, a)
80
+ emit({ ...a, ...more })
81
+ }
82
+ return {
83
+ has: id => agents.has(id),
84
+ /** The Agent call: it starts. */
85
+ called(id, input, parent) {
86
+ send(agentFrom(id, input, parent))
87
+ },
88
+ /** One of its own calls: the first path inside a worktree says which one it works in. */
89
+ touched(id, input) {
90
+ const a = agents.get(id)
91
+ if (!a || a.worktree) return
92
+ const path = pathsIn(input).map(worktreeOf).find(Boolean)
93
+ if (!path) return
94
+ send({ ...a, worktree: { path: display(path) } })
95
+ void Promise.resolve(branchOf(path)).then(branch => {
96
+ const now = agents.get(id)
97
+ if (branch && now?.worktree && !now.worktree.branch) send({ ...now, worktree: { ...now.worktree, branch } })
98
+ })
99
+ },
100
+ /** A one-line summary of where it's at, while it works: shown live, never stored. */
101
+ progress(id, summary) {
102
+ const a = agents.get(id)
103
+ if (a && summary) emit({ ...a, summary: String(summary).slice(0, 200), transient: true })
104
+ },
105
+ /** Its call's result: done, or still going in the background. */
106
+ returned(id, result, failed) {
107
+ const a = agents.get(id)
108
+ if (a) send(finished(a, result, failed))
109
+ },
110
+ /** A background agent ended (task_notification). */
111
+ ended(id, status, summary) {
112
+ const a = agents.get(id)
113
+ if (a?.status !== 'running') return
114
+ send({ ...a, status: ENDED[status] ?? 'done', ...(summary ? { summary: String(summary).slice(0, 200) } : {}) })
115
+ },
116
+ /** The turn ended: whatever still runs stops with it. */
117
+ stopAll() {
118
+ for (const a of agents.values()) if (a.status === 'running') send({ ...a, status: 'stopped' })
119
+ },
120
+ }
121
+ }