kira-glasses-bridge 2.19.0 → 2.21.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.19.0",
3
+ "version": "2.21.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": {
@@ -0,0 +1,29 @@
1
+ // Claude Code on this Mac, as the person signed in to it: which account it uses, and one piece
2
+ // of text written with that sign-in (complete.mjs). The bridge's calls for both.
3
+ import { execFile } from 'node:child_process'
4
+ import { completeText } from './complete.mjs'
5
+
6
+ /** Which Claude account Claude Code on this Mac is signed in to (`claude auth status --json`). */
7
+ async function claudeAccount() {
8
+ const bin = process.env.KIRA_CLAUDE_BIN || 'claude'
9
+ return new Promise(resolve =>
10
+ execFile(bin, ['auth', 'status', '--json'], { timeout: 10_000 }, (err, stdout) => {
11
+ try {
12
+ const s = JSON.parse(stdout)
13
+ resolve({
14
+ loggedIn: !!s.loggedIn,
15
+ email: s.email ?? null,
16
+ plan: s.subscriptionType ?? null,
17
+ org: s.orgName ?? null,
18
+ method: s.authMethod ?? null,
19
+ })
20
+ } catch {
21
+ const missing = err?.code === 'ENOENT'
22
+ const why = missing ? 'Claude Code is not installed on this Mac' : 'Claude Code did not say which account'
23
+ resolve({ error: why })
24
+ }
25
+ }),
26
+ )
27
+ }
28
+
29
+ export const claudeTools = { claude_account: claudeAccount, complete_text: completeText }
@@ -0,0 +1,59 @@
1
+ // One piece of text from the person's own Claude Code, with their own sign-in (no API key): a task
2
+ // brief, a recording's notes, an automation's thinking step. One turn, no tools, nothing written:
3
+ // the API asks this when their Mac is online, and uses an API key only when it isn't.
4
+ import { query } from '@anthropic-ai/claude-agent-sdk'
5
+ import { mkdirSync } from 'node:fs'
6
+ import { homedir } from 'node:os'
7
+ import { join } from 'node:path'
8
+
9
+ /** Everything Claude Code could reach for: text only here. */
10
+ const NO_TOOLS = [
11
+ 'Read', 'Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'Bash', 'Glob', 'Grep', 'LS', 'WebFetch', 'WebSearch',
12
+ 'Task', 'Agent', 'TodoWrite', 'ToolSearch', 'Skill', 'ExitPlanMode', 'ListMcpResourcesTool', 'ReadMcpResourceTool',
13
+ ]
14
+ const MAX_MS = 170_000
15
+
16
+ /** Its own folder, so these one-off turns don't land among the person's sessions for real work. */
17
+ function folder() {
18
+ const dir = join(homedir(), '.kira', 'text')
19
+ mkdirSync(dir, { recursive: true })
20
+ return dir
21
+ }
22
+
23
+ export async function completeText(args) {
24
+ const system = String(args?.system ?? '').trim()
25
+ const user = String(args?.user ?? '').trim()
26
+ if (!user) return { error: 'user is required' }
27
+ const abort = new AbortController()
28
+ const timer = setTimeout(() => abort.abort(), Math.min(Number(args?.timeout_ms) || 60_000, MAX_MS))
29
+ try {
30
+ const q = query({
31
+ prompt: user,
32
+ options: {
33
+ cwd: folder(),
34
+ env: { ...process.env, ENABLE_CLAUDEAI_MCP_SERVERS: 'false' },
35
+ ...(system ? { systemPrompt: system } : {}),
36
+ disallowedTools: NO_TOOLS,
37
+ canUseTool: async () => ({ behavior: 'deny', message: 'Write the answer as text; no tools here.' }),
38
+ // Claude's best for writing that people read; the API can ask for another ("sonnet", "haiku").
39
+ model: String(args?.model ?? 'opus'),
40
+ maxTurns: 1,
41
+ settingSources: [],
42
+ abortController: abort,
43
+ ...(process.env.KIRA_CLAUDE_BIN ? { pathToClaudeCodeExecutable: process.env.KIRA_CLAUDE_BIN } : {}),
44
+ },
45
+ })
46
+ let model = null
47
+ for await (const m of q) {
48
+ if (m.type === 'system' && m.subtype === 'init') model = m.model ?? null
49
+ if (m.type !== 'result') continue
50
+ if (m.subtype !== 'success') return { error: `Claude stopped: ${m.subtype}` }
51
+ return { text: String(m.result ?? '').trim(), model }
52
+ }
53
+ return { error: 'Claude ended without an answer' }
54
+ } catch (err) {
55
+ return { error: abort.signal.aborted ? 'Claude took too long' : `could not run Claude: ${err.message}` }
56
+ } finally {
57
+ clearTimeout(timer)
58
+ }
59
+ }
@@ -31,11 +31,60 @@ export const WEB = new Set(['WebFetch', 'WebSearch'])
31
31
  /** Claude Code's name for the tools of Kira's MCP server (mcp__<server>__<tool>). */
32
32
  export const KIRA_TOOLS = 'mcp__kira__'
33
33
 
34
- /** Read-only commands: looking, listing, testing, type-checking, building. Nothing that writes, sends or installs. */
35
- const SAFE_CMD = /^(?:ls|ll|pwd|cat|head|tail|wc|echo|printf|which|type|env|printenv|date|whoami|id|uname|tree|find|fd|grep|rg|ag|awk|cut|tr|sort|uniq|diff|comm|stat|file|du|df|basename|dirname|realpath|readlink|jq|yq|xxd|hexdump|strings|test|\[|true|false|sleep|md5|shasum|sha256sum|md5sum|column|nl|less|more|open -n --args|sed -n|sed -E -n|sed -n -E|git (?:status|diff|log|show|branch|rev-parse|remote|ls-files|ls-tree|blame|describe|stash list|tag|config --get|config -l|cat-file|grep|shortlog|count-objects|reflog)|node (?:-v|--version|-e|-p|--eval|--print)|npm (?:-v|--version|ls|list|view|outdated|why|test|run (?:test|lint|typecheck|check|build|format:check))|pnpm (?:-v|--version|ls|list|why|outdated|test|typecheck|lint|build|run (?:test|lint|typecheck|check|build))|yarn (?:-v|--version|list|test|lint|typecheck|build)|npx (?:tsc|eslint|prettier --check|jest|vitest|playwright test)|tsc|eslint|prettier --check|jest|vitest|cargo (?:check|test|build|fmt --check|clippy|tree|metadata)|go (?:test|build|vet|version|list|env)|python3? (?:-V|--version|-m pytest|-c)|pytest|ruby -v|make (?:test|check|lint|build)|mvn (?:test|compile)|gradle (?:test|build)|swift (?:build|test)|xcodebuild -list|brew (?:list|info|--version)|docker (?:ps|images|version)|gh (?:pr (?:list|view|status|checks)|issue (?:list|view)|run (?:list|view)|repo view|api)|heroku (?:logs|ps|releases|config(?::get)?|apps|info))(?:\s|$)/
34
+ /**
35
+ * Read-only commands: looking, listing, testing, type-checking, building. Nothing that writes, sends
36
+ * or installs, nothing that runs code given inline (node -e, python -c, awk), nothing that prints
37
+ * secrets (heroku config).
38
+ */
39
+ /** Plain commands that only look. */
40
+ const LOOKERS = [
41
+ 'ls', 'll', 'pwd', 'cat', 'head', 'tail', 'wc', 'echo', 'printf', 'which', 'type', 'env', 'printenv',
42
+ 'date', 'whoami', 'id', 'uname', 'tree', 'find', 'fd', 'grep', 'rg', 'ag', 'cut', 'tr', 'sort', 'uniq',
43
+ 'diff', 'comm', 'stat', 'file', 'du', 'df', 'basename', 'dirname', 'realpath', 'readlink', 'jq', 'yq',
44
+ 'xxd', 'hexdump', 'strings', 'test', '[', 'true', 'false', 'sleep', 'md5', 'shasum', 'sha256sum',
45
+ 'md5sum', 'column', 'nl', 'less', 'more', 'tsc', 'eslint', 'jest', 'vitest', 'pytest',
46
+ ]
47
+ /** Tools with their read-only, test and build subcommands. */
48
+ const SUBCOMMANDS = {
49
+ git: [
50
+ 'status', 'diff', 'log', 'show', 'branch', 'rev-parse', 'remote', 'ls-files', 'ls-tree', 'blame', 'describe',
51
+ 'stash list', 'tag', 'config --get', 'config -l', 'cat-file', 'grep', 'shortlog', 'count-objects', 'reflog',
52
+ ],
53
+ node: ['-v', '--version'],
54
+ npm: ['-v', '--version', 'ls', 'list', 'view', 'outdated', 'why', 'test', 'run test', 'run lint', 'run typecheck',
55
+ 'run check', 'run build', 'run format:check'],
56
+ pnpm: ['-v', '--version', 'ls', 'list', 'why', 'outdated', 'test', 'typecheck', 'lint', 'build', 'run test',
57
+ 'run lint', 'run typecheck', 'run check', 'run build'],
58
+ yarn: ['-v', '--version', 'list', 'test', 'lint', 'typecheck', 'build'],
59
+ npx: ['tsc', 'eslint', 'prettier --check', 'jest', 'vitest', 'playwright test'],
60
+ prettier: ['--check'],
61
+ cargo: ['check', 'test', 'build', 'fmt --check', 'clippy', 'tree', 'metadata'],
62
+ go: ['test', 'build', 'vet', 'version', 'list', 'env'],
63
+ python: ['-V', '--version', '-m pytest'],
64
+ python3: ['-V', '--version', '-m pytest'],
65
+ ruby: ['-v'],
66
+ make: ['test', 'check', 'lint', 'build'],
67
+ mvn: ['test', 'compile'],
68
+ gradle: ['test', 'build'],
69
+ swift: ['build', 'test'],
70
+ xcodebuild: ['-list'],
71
+ brew: ['list', 'info', '--version'],
72
+ docker: ['ps', 'images', 'version'],
73
+ gh: ['pr list', 'pr view', 'pr status', 'pr checks', 'issue list', 'issue view', 'run list', 'run view', 'repo view'],
74
+ heroku: ['logs', 'ps', 'releases', 'apps', 'info'],
75
+ open: ['-n --args'],
76
+ sed: ['-n', '-E -n', '-n -E'],
77
+ }
78
+ const escape = w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
79
+ const withSubs = Object.entries(SUBCOMMANDS).flatMap(([tool, subs]) => subs.map(sub => `${tool} ${sub}`))
80
+ const SAFE_PHRASES = [...LOOKERS, ...withSubs]
81
+ const SAFE_CMD = new RegExp(`^(?:${SAFE_PHRASES.map(escape).join('|')})(?:\\s|$)`)
36
82
 
37
83
  /** Redirections that write somewhere other than nowhere. */
38
84
  const WRITES = /(^|[^&\d])>(?!&)|>>|\btee\b|\bxargs\b|\beval\b|\bexec\b|`/
85
+ /** find and fd can run or delete what they find: then they aren't looking any more. */
86
+ const FINDER_ACTS =
87
+ /^(?:find|fd)\b.*\s(?:-exec(?:dir)?|-ok(?:dir)?|-delete|-fprint\w*|-fls|-x|--exec(?:-batch)?|-X)\b/
39
88
 
40
89
  function pieces(command) {
41
90
  // Strip quoted strings so separators inside them do not split; keep a placeholder.
@@ -76,7 +125,8 @@ export function shellIsSafe(command, allowRules = []) {
76
125
  c = c.slice(0, m.index) + '""' + c.slice(m.index + m[0].length)
77
126
  }
78
127
  if (WRITES.test(c.replace(/2>&1|2>\/dev\/null|>\/dev\/null|&>\/dev\/null/g, ''))) return false
79
- return pieces(c).every(p => SAFE_CMD.test(p) || allowRules.some(r => r && (p === r || p.startsWith(String(r) + ' '))))
128
+ const allowedByRule = p => allowRules.some(r => r && (p === r || p.startsWith(String(r) + ' ')))
129
+ return pieces(c).every(p => (SAFE_CMD.test(p) && !FINDER_ACTS.test(p)) || allowedByRule(p))
80
130
  }
81
131
 
82
132
  /** The rule key an "always allow" answer produces. */
package/src/index.mjs CHANGED
@@ -13,6 +13,7 @@ import { openApp, installedApps } from './apps.mjs'
13
13
  import { listFiles, readUserFile, createFolder, writeUserFile, trashPath, resolveUserPath } from './files.mjs'
14
14
  import { initTerminals, terminalCall, terminalMessage, terminalsOffline } from './terminals.mjs'
15
15
  import { gitTools } from './git-tools.mjs'
16
+ import { claudeTools } from './agents/claude-local.mjs'
16
17
 
17
18
  import { createRequire } from 'node:module'
18
19
  const { version: BRIDGE_VERSION } = createRequire(import.meta.url)('../package.json')
@@ -212,22 +213,7 @@ end tell`
212
213
  },
213
214
 
214
215
  ...gitTools,
215
-
216
- /** Which Claude account Claude Code on this Mac is signed in to (`claude auth status --json`). */
217
- async claude_account() {
218
- const { execFile } = await import('node:child_process')
219
- const bin = process.env.KIRA_CLAUDE_BIN || 'claude'
220
- return new Promise(resolve =>
221
- execFile(bin, ['auth', 'status', '--json'], { timeout: 10_000 }, (err, stdout) => {
222
- try {
223
- const s = JSON.parse(stdout)
224
- resolve({ loggedIn: !!s.loggedIn, email: s.email ?? null, plan: s.subscriptionType ?? null, org: s.orgName ?? null, method: s.authMethod ?? null })
225
- } catch {
226
- resolve({ error: err?.code === 'ENOENT' ? 'Claude Code is not installed on this Mac' : 'Claude Code did not say which account it uses' })
227
- }
228
- }),
229
- )
230
- },
216
+ ...claudeTools,
231
217
 
232
218
 
233
219
  }