kira-glasses-bridge 2.36.0 → 2.37.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.36.0",
3
+ "version": "2.37.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": {
package/src/chrome.mjs ADDED
@@ -0,0 +1,104 @@
1
+ // Chrome on this Mac, for Links: each profile's bookmarks, read from the Bookmarks file Chrome keeps
2
+ // synced with the Google account signed in to it, and the tabs open right now. Read only: Chrome
3
+ // rewrites that file from its own copy while it runs, so Kira never writes to it.
4
+ import { execFile } from 'node:child_process'
5
+ import { readFile } from 'node:fs/promises'
6
+ import { homedir } from 'node:os'
7
+ import { join } from 'node:path'
8
+
9
+ const CHROME_DIR = join(homedir(), 'Library', 'Application Support', 'Google', 'Chrome')
10
+ const SEP = ' › '
11
+
12
+ /** Chrome's times are microseconds since 1601; an ISO date, or null when there's none. */
13
+ export function chromeTime(raw) {
14
+ const micros = Number(raw)
15
+ if (!Number.isFinite(micros) || micros <= 0) return null
16
+ return new Date(micros / 1000 - 11_644_473_600_000).toISOString()
17
+ }
18
+
19
+ /** The profiles in Chrome's Local State: folder, the name Chrome shows, and the Google account. */
20
+ export function profilesOf(localState) {
21
+ const cache = localState?.profile?.info_cache ?? {}
22
+ return Object.entries(cache).map(([profile, info]) => ({
23
+ profile,
24
+ name: String(info?.name || profile),
25
+ account: info?.user_name ? String(info.user_name) : null,
26
+ }))
27
+ }
28
+
29
+ /** Every bookmark in a Bookmarks file, flat, each with its folders from the root ("Bookmarks bar › Dev"). */
30
+ export function flattenBookmarks(file) {
31
+ const out = []
32
+ const walk = (node, path) => {
33
+ if (node?.type === 'url' && /^https?:/i.test(node.url ?? '')) {
34
+ out.push({
35
+ chromeId: String(node.guid || node.id),
36
+ url: node.url,
37
+ title: String(node.name ?? ''),
38
+ folder: path.join(SEP),
39
+ addedAt: chromeTime(node.date_added),
40
+ })
41
+ }
42
+ for (const child of node?.children ?? []) walk(child, node.type === 'folder' ? [...path, node.name] : path)
43
+ }
44
+ for (const root of Object.values(file?.roots ?? {})) {
45
+ if (root && typeof root === 'object' && root.type === 'folder') {
46
+ for (const child of root.children ?? []) walk(child, [root.name])
47
+ }
48
+ }
49
+ return out
50
+ }
51
+
52
+ const readJson = async path => JSON.parse(await readFile(path, 'utf8'))
53
+
54
+ /** Each profile with its bookmarks; none when Chrome isn't installed. */
55
+ async function bookmarks() {
56
+ let state
57
+ try {
58
+ state = await readJson(join(CHROME_DIR, 'Local State'))
59
+ } catch {
60
+ return { profiles: [] }
61
+ }
62
+ const profiles = await Promise.all(
63
+ profilesOf(state).map(async p => {
64
+ const file = await readJson(join(CHROME_DIR, p.profile, 'Bookmarks')).catch(() => null)
65
+ return { ...p, bookmarks: file ? flattenBookmarks(file) : [] }
66
+ }),
67
+ )
68
+ return { profiles }
69
+ }
70
+
71
+ // Asks Chrome only when it's already running (running() doesn't start it).
72
+ const TABS_SCRIPT = `
73
+ const chrome = Application('Google Chrome')
74
+ chrome.running()
75
+ ? JSON.stringify(chrome.windows().flatMap(w => w.tabs().map(t => ({ url: t.url(), title: t.title() }))))
76
+ : '[]'`
77
+
78
+ /** The tabs open in Chrome now. The first time, macOS asks whether Kira may control Chrome. */
79
+ function tabs() {
80
+ return new Promise(resolve => {
81
+ execFile('osascript', ['-l', 'JavaScript', '-e', TABS_SCRIPT], { timeout: 10_000 }, (err, stdout) => {
82
+ if (err) {
83
+ const denied = /-1743|not allowed/i.test(String(err.message))
84
+ const error = denied
85
+ ? 'Kira isn’t allowed to read Chrome’s tabs (System Settings › Privacy › Automation).'
86
+ : 'Chrome’s tabs couldn’t be read.'
87
+ return resolve({ tabs: [], error })
88
+ }
89
+ try {
90
+ const list = JSON.parse(stdout.trim() || '[]').filter(t => /^https?:/i.test(t.url ?? ''))
91
+ resolve({ tabs: list })
92
+ } catch {
93
+ resolve({ tabs: [] })
94
+ }
95
+ })
96
+ })
97
+ }
98
+
99
+ /** The bridge's `chrome` tool: { op: 'bookmarks' } or { op: 'tabs' }. */
100
+ export function chromeCall(args) {
101
+ if (args?.op === 'bookmarks') return bookmarks()
102
+ if (args?.op === 'tabs') return tabs()
103
+ return Promise.resolve({ error: `unknown op ${args?.op}` })
104
+ }
package/src/mac-tools.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // The bridge's tools for this Mac itself: its apps, a terminal, a project's folder (found or new), and
2
- // (inside the Mac app only) its shells for other devices and its browser dock.
2
+ // (inside the Mac app only) its shells for other devices and its browser dock, and Chrome's bookmarks and tabs.
3
3
  import { execFile } from 'node:child_process'
4
4
  import { openApp, installedApps } from './apps.mjs'
5
5
  import { resolveUserPath } from './files.mjs'
@@ -8,6 +8,7 @@ import { findCheckouts } from './checkouts.mjs'
8
8
  import { terminalCall } from './terminals.mjs'
9
9
  import { safeCommand, terminalScript } from './terminal-command.mjs'
10
10
  import { host } from './host.mjs'
11
+ import { chromeCall } from './chrome.mjs'
11
12
 
12
13
  export const macTools = {
13
14
  async open_app(args) {
@@ -39,6 +40,9 @@ export const macTools = {
39
40
  /** This Mac's shells for a browser elsewhere (only inside the Mac app, only when switched on there). */
40
41
  terminal: args => terminalCall(args),
41
42
 
43
+ /** Chrome's bookmarks (every profile) and open tabs, for Links. */
44
+ chrome: args => chromeCall(args),
45
+
42
46
  /** Kira in the Mac app's browser dock: open a page, read it, click and type in it. */
43
47
  async browser(args) {
44
48
  const hooks = host()