kira-glasses-bridge 2.14.0 → 2.15.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.14.0",
3
+ "version": "2.15.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": {
@@ -191,11 +191,24 @@ export async function run({ prompt, sessionId, cwd, policy, persona, abort }, io
191
191
  return applyAnswer(toolName, input, request, resolved.answer, suggestions)
192
192
  }
193
193
 
194
+ // Background work (subagents, `run_in_background` shell commands, monitors)
195
+ // started by the model would outlive the turn: this is a one-shot run, so the
196
+ // moment the main thread stops the result is released, the process ends and
197
+ // that work is killed, never having reported. Claude's parting words ("the
198
+ // agents are still running; I'll compile the report when they finish") then
199
+ // became the job's final report, and Kira told the user it was still going.
200
+ // With this set, the tool has no background option: subagents run inline and
201
+ // their findings are in the answer.
202
+ const env = { ...process.env, CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: '1' }
203
+ /** Background tasks the model still managed to start (an older CLI ignoring the flag): tool_use id -> description. */
204
+ const orphans = new Map()
205
+
194
206
  try {
195
207
  const q = query({
196
208
  prompt,
197
209
  options: {
198
210
  cwd,
211
+ env,
199
212
  ...(sessionId ? { resume: sessionId } : {}),
200
213
  permissionMode: 'default',
201
214
  ...(abort ? { abortController: abort } : {}),
@@ -211,18 +224,26 @@ export async function run({ prompt, sessionId, cwd, policy, persona, abort }, io
211
224
  },
212
225
  })
213
226
  for await (const m of q) {
227
+ // Messages from inside a subagent carry the id of the Subtask call that started it.
228
+ const nested = !!m.parent_tool_use_id
214
229
  if (m.type === 'system' && m.subtype === 'init') {
215
230
  session = m.session_id ?? session
216
231
  io.emit({ kind: 'session', sessionId: session, model: m.model ?? null })
232
+ } else if (m.type === 'system' && m.subtype === 'task_started') {
233
+ if (m.is_backgrounded && m.tool_use_id) orphans.set(m.tool_use_id, m.description ?? m.subagent_type ?? 'task')
234
+ } else if (m.type === 'system' && m.subtype === 'task_notification') {
235
+ if (m.tool_use_id) orphans.delete(m.tool_use_id)
217
236
  } else if (m.type === 'assistant') {
218
237
  for (const b of m.message?.content ?? []) {
219
- if (b.type === 'text' && b.text?.trim()) io.emit({ kind: 'message', text: short(b.text, TEXT_CAP) })
238
+ // A subagent's narration is for its parent, not the user; its actions still show what is going on while the Subtask runs.
239
+ if (b.type === 'text' && b.text?.trim() && !nested) io.emit({ kind: 'message', text: short(b.text, TEXT_CAP) })
220
240
  else if (b.type === 'tool_use') io.emit({ kind: 'action', id: b.id, tool: b.name, ...describeAction(b.name, b.input, cwd) })
221
241
  }
222
242
  } else if (m.type === 'user') {
223
243
  for (const b of m.message?.content ?? []) {
224
244
  if (b?.type === 'tool_result') {
225
245
  const t = typeof b.content === 'string' ? b.content : Array.isArray(b.content) ? b.content.map(c => (c.type === 'text' ? c.text : '')).join('\n') : ''
246
+ if (/^Async agent launched successfully/.test(t.trim()) && !orphans.has(b.tool_use_id)) orphans.set(b.tool_use_id, 'subagent')
226
247
  io.emit({ kind: 'action_result', id: b.tool_use_id, ok: !b.is_error, preview: short(t.trim(), 240) })
227
248
  }
228
249
  }
@@ -230,6 +251,11 @@ export async function run({ prompt, sessionId, cwd, policy, persona, abort }, io
230
251
  session = m.session_id ?? session
231
252
  if (m.subtype === 'success') text = String(m.result ?? '')
232
253
  else error = `Claude stopped: ${m.subtype}${Array.isArray(m.errors) && m.errors.length ? ` (${m.errors.join('; ').slice(0, 300)})` : ''}`
254
+ // Work left running in the background dies with this process; say so rather than let "still running" stand as the outcome.
255
+ if (orphans.size) {
256
+ const note = `${orphans.size} background ${orphans.size === 1 ? 'task was' : 'tasks were'} still running when Claude ended its turn (${[...orphans.values()].slice(0, 5).join(', ')}); they were stopped with the session and their findings are not in this report. Send a follow-up to have that work done inline.`
257
+ text = text ? `${text}\n\n(${note})` : `(${note})`
258
+ }
233
259
  const stats = { durationMs: m.duration_ms, turns: m.num_turns, costUsd: m.total_cost_usd }
234
260
  io.emit({ kind: 'result', ok: !error, text: short(text, TEXT_CAP), error, sessionId: session, stats })
235
261
  return { ok: !error, text, error, sessionId: session, stats }
@@ -155,14 +155,16 @@ async function designsOf(s) {
155
155
  }
156
156
 
157
157
  /**
158
- * Scan every session changed since `since` (ISO time; all of them when omitted). Returns the
159
- * designs without their HTML, plus the HTML of those whose hash isn't in `known` (hashes the API
160
- * already holds), so a rescan sends only what changed.
158
+ * Scan every session changed since `since` (ISO time; all of them when omitted), or only the
159
+ * sessions named in `sessions` (a Claude job just finished in one). Returns the designs without
160
+ * their HTML, plus the HTML of those whose hash isn't in `known` (hashes the API already holds),
161
+ * so a rescan sends only what changed.
161
162
  */
162
- export async function scanDesigns({ since, known = [] } = {}) {
163
+ export async function scanDesigns({ since, known = [], sessions: only } = {}) {
163
164
  const after = since ? Date.parse(since) || 0 : 0
164
165
  const have = new Set(known)
165
- const sessions = (await sdkListSessions()).filter(s => s.lastModified > after)
166
+ const pick = Array.isArray(only) && only.length ? new Set(only) : null
167
+ const sessions = (await sdkListSessions()).filter(s => (pick ? pick.has(s.sessionId) : s.lastModified > after))
166
168
  const designs = []
167
169
  let failed = 0
168
170
  for (const s of sessions) {
package/src/index.mjs CHANGED
@@ -45,6 +45,7 @@ const ANSWER_TIMEOUT_MS = 3 * 60 * 1000
45
45
  * openTerminal({ cwd, command }) open_terminal in the app's own terminal (a Kira tab), not
46
46
  * Terminal.app; resolves to what to report, or null to fall back
47
47
  * terminals the app's shells, for remote devices (terminals.mjs)
48
+ * browser({ op, … }) Kira in the app's browser dock: open, read, click, type (browser-kira.mjs)
48
49
  */
49
50
  let hooks = {}
50
51
  let stopped = false
@@ -112,7 +113,7 @@ const tools = {
112
113
  return listProjects(args.max)
113
114
  },
114
115
  async scan_claude_designs(args) {
115
- return scanDesigns({ since: args.since, known: Array.isArray(args.known) ? args.known : [] })
116
+ return scanDesigns({ since: args.since, known: Array.isArray(args.known) ? args.known : [], sessions: args.sessions })
116
117
  },
117
118
  async list_claude_sessions(args) {
118
119
  return listSessions(args.max)
@@ -198,6 +199,12 @@ const tools = {
198
199
  /** This Mac's shells for a browser elsewhere (only inside the Mac app, only when switched on there). */
199
200
  terminal: args => terminalCall(args),
200
201
 
202
+ /** Kira in the Mac app's browser dock: open a page, read it, click and type in it. */
203
+ async browser(args) {
204
+ if (!hooks.browser) return { error: 'the browser needs the Kira Mac app (this bridge has none)', reason: 'unavailable' }
205
+ return hooks.browser(args)
206
+ },
207
+
201
208
  /** Open a terminal on this Mac in a folder and run a command there (used to hand a Claude session
202
209
  * to the person): a Kira tab when the bridge runs inside the Mac app, else Terminal.app. */
203
210
  async open_terminal(args) {