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 +1 -1
- package/src/agents/claude-local.mjs +29 -0
- package/src/agents/complete.mjs +59 -0
- package/src/agents/policy.mjs +53 -3
- package/src/index.mjs +2 -16
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/agents/policy.mjs
CHANGED
|
@@ -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
|
-
/**
|
|
35
|
-
|
|
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
|
-
|
|
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
|
}
|