kira-glasses-bridge 2.24.0 → 2.25.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/README.md +1 -1
- package/package.json +1 -1
- package/src/agent-files.mjs +38 -6
- package/src/cli.mjs +1 -1
- package/src/config-folders.mjs +22 -2
- package/src/index.mjs +2 -1
- package/src/skill-files.mjs +20 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Connects your Mac to **Kira**, the assistant for Even Realities G2 glasses. With
|
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
|
-
1. In the Kira portal, open **
|
|
7
|
+
1. In the Kira portal, open **Settings → Devices** and choose **Set up with Terminal**, then copy the command it shows.
|
|
8
8
|
2. Paste it into Terminal:
|
|
9
9
|
|
|
10
10
|
```bash
|
package/package.json
CHANGED
package/src/agent-files.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
exists,
|
|
13
13
|
field,
|
|
14
14
|
inside,
|
|
15
|
+
oneAtATime,
|
|
15
16
|
readSmall,
|
|
16
17
|
refuseLinks,
|
|
17
18
|
splitFront,
|
|
@@ -19,8 +20,13 @@ import {
|
|
|
19
20
|
} from './config-folders.mjs'
|
|
20
21
|
|
|
21
22
|
const MAX_MD = 256 * 1024
|
|
22
|
-
/**
|
|
23
|
+
/** A MEMORY.md Kira reads or replaces whole (from the agent's editor) is at most this. */
|
|
23
24
|
const MAX_MEMORY = 64 * 1024
|
|
25
|
+
/** Claude Code loads the first 200 lines or 25KB of MEMORY.md: what Kira adds stays inside that. */
|
|
26
|
+
const LOAD_LINES = 200
|
|
27
|
+
const LOAD_BYTES = 25 * 1024
|
|
28
|
+
/** A line Kira added after a task (rememberAgent): "- 2026-10-12: …". The only lines she ever drops. */
|
|
29
|
+
const KIRAS_LINE = /^- \d{4}-\d{2}-\d{2}: /
|
|
24
30
|
const WHAT = 'a subagent'
|
|
25
31
|
const NAMED = { loose: true }
|
|
26
32
|
const claudeRoot = () => agentFolders()[0].root
|
|
@@ -113,19 +119,45 @@ export async function removeAgent({ slug } = {}, root = claudeRoot()) {
|
|
|
113
119
|
/** Replace a subagent's MEMORY.md, whole (`name` is its front-matter name). */
|
|
114
120
|
export async function writeAgentMemory({ name, md } = {}, root = FOLDERS.claude.agentMemory()) {
|
|
115
121
|
const file = memoryFile(name, root)
|
|
116
|
-
|
|
122
|
+
return oneAtATime(file, () => writeMemoryFile(name, file, String(md ?? ''), root))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function writeMemoryFile(name, file, text, root) {
|
|
117
126
|
if (Buffer.byteLength(text) > MAX_MEMORY) throw new Error('That memory is too large')
|
|
118
127
|
await refuseLinks([root, join(file, '..'), file], 'edit')
|
|
119
128
|
await writeWhole(file, text)
|
|
120
129
|
return { name, path: display(file), memory: text }
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* MEMORY.md with `entry` added at the end, kept inside what Claude Code loads (200 lines, 25KB) by
|
|
134
|
+
* dropping Kira's own oldest dated lines, never the person's. Null when it can't fit without
|
|
135
|
+
* dropping theirs (then it isn't added: past the window Claude Code wouldn't read it anyway).
|
|
136
|
+
*/
|
|
137
|
+
export function withEntry(text, entry) {
|
|
138
|
+
const lines = text.trimEnd() ? text.trimEnd().split('\n') : []
|
|
139
|
+
lines.push(entry)
|
|
140
|
+
const fits = () => lines.length <= LOAD_LINES && Buffer.byteLength(`${lines.join('\n')}\n`) <= LOAD_BYTES
|
|
141
|
+
while (!fits()) {
|
|
142
|
+
const oldest = lines.findIndex((l, i) => i < lines.length - 1 && KIRAS_LINE.test(l))
|
|
143
|
+
if (oldest < 0) return null
|
|
144
|
+
lines.splice(oldest, 1)
|
|
145
|
+
}
|
|
146
|
+
return `${lines.join('\n')}\n`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Add a line to a subagent's MEMORY.md (what Kira keeps after a task); see withEntry. */
|
|
124
150
|
export async function rememberAgent({ name, line } = {}, root = FOLDERS.claude.agentMemory()) {
|
|
151
|
+
const file = memoryFile(name, root)
|
|
125
152
|
const entry = `- ${String(line ?? '').replace(/\s+/g, ' ').trim().slice(0, 400)}`
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
153
|
+
return oneAtATime(file, async () => {
|
|
154
|
+
const was = await readSmall(file, MAX_MEMORY)
|
|
155
|
+
// One there that's too big to read is theirs, whole: left as it is.
|
|
156
|
+
if (was === null && (await exists(file))) return { name, path: display(file), memory: null, full: true }
|
|
157
|
+
const text = withEntry(was ?? '', entry)
|
|
158
|
+
if (text === null) return { name, path: display(file), memory: was, full: true }
|
|
159
|
+
return writeMemoryFile(name, file, text, root)
|
|
160
|
+
})
|
|
129
161
|
}
|
|
130
162
|
|
|
131
163
|
/** The bridge tools (index.mjs). */
|
package/src/cli.mjs
CHANGED
|
@@ -42,7 +42,7 @@ folders and apps, by voice.
|
|
|
42
42
|
kira-bridge logs follow the service log
|
|
43
43
|
kira-bridge install | uninstall manage the background service
|
|
44
44
|
|
|
45
|
-
Get a token from the Kira portal:
|
|
45
|
+
Get a token from the Kira portal: Settings -> Devices -> Set up with Terminal.
|
|
46
46
|
Settings are kept in ~/.kira/bridge.json.`
|
|
47
47
|
|
|
48
48
|
async function main() {
|
package/src/config-folders.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Codex (developers.openai.com/codex: skills, subagents): ~/.agents/skills/<name>/SKILL.md (the
|
|
11
11
|
// shared Agent Skills folder) and ~/.codex/agents/<name>.toml. ~/.codex/skills isn't in its docs,
|
|
12
12
|
// but the Codex app keeps its own skills there, so Kira lists them as Codex's.
|
|
13
|
+
import { randomBytes } from 'node:crypto'
|
|
13
14
|
import { lstat, mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises'
|
|
14
15
|
import { homedir } from 'node:os'
|
|
15
16
|
import { join, relative } from 'node:path'
|
|
@@ -90,15 +91,34 @@ export const exists = p => stat(p).then(
|
|
|
90
91
|
() => false,
|
|
91
92
|
)
|
|
92
93
|
|
|
93
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Write `text` to `file` whole: its folder made, through a temporary file renamed over the old one
|
|
96
|
+
* (named for this write alone, so two writes never share one).
|
|
97
|
+
*/
|
|
94
98
|
export async function writeWhole(file, text) {
|
|
95
99
|
const dir = join(file, '..')
|
|
96
100
|
await mkdir(dir, { recursive: true })
|
|
97
|
-
const tmp = join(dir, `.${file.slice(dir.length + 1)}.${process.pid}.tmp`)
|
|
101
|
+
const tmp = join(dir, `.${file.slice(dir.length + 1)}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`)
|
|
98
102
|
await writeFile(tmp, text, 'utf8')
|
|
99
103
|
await rename(tmp, file)
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
/** Per file: the change running on it, so a read-modify-write never interleaves with another. */
|
|
107
|
+
const queues = new Map()
|
|
108
|
+
|
|
109
|
+
/** Run `fn` once every change queued before it on `key` (a file's path) is done. */
|
|
110
|
+
export function oneAtATime(key, fn) {
|
|
111
|
+
const before = queues.get(key) ?? Promise.resolve()
|
|
112
|
+
const run = before.then(fn, fn)
|
|
113
|
+
const settled = run.then(
|
|
114
|
+
() => {},
|
|
115
|
+
() => {},
|
|
116
|
+
)
|
|
117
|
+
queues.set(key, settled)
|
|
118
|
+
settled.then(() => queues.get(key) === settled && queues.delete(key))
|
|
119
|
+
return run
|
|
120
|
+
}
|
|
121
|
+
|
|
102
122
|
/** Front matter between --- lines, and the rest. */
|
|
103
123
|
export function splitFront(md) {
|
|
104
124
|
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(md)
|
package/src/index.mjs
CHANGED
|
@@ -43,7 +43,8 @@ export function startBridge(config, hostHooks = {}) {
|
|
|
43
43
|
if (config.claudeBin) process.env.KIRA_CLAUDE_BIN = config.claudeBin
|
|
44
44
|
if (!config.url || !config.token) {
|
|
45
45
|
throw new Error(
|
|
46
|
-
'No token. Run: npx kira-glasses-bridge --token <token>
|
|
46
|
+
'No token. Run: npx kira-glasses-bridge --token <token> ' +
|
|
47
|
+
'(from the Kira portal → Settings → Devices → Set up with Terminal)',
|
|
47
48
|
)
|
|
48
49
|
}
|
|
49
50
|
// Terminal output is live or nothing: never queued for later like job events.
|
package/src/skill-files.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// in Codex's (config-folders.mjs says which loads which). Kira lists all of them, each with the
|
|
3
3
|
// tools that load it (a skill linked into both folders is both's), and writes and removes the ones
|
|
4
4
|
// a person makes in Kira, in Claude Code's folder.
|
|
5
|
-
import { readdir, realpath, rmdir, unlink } from 'node:fs/promises'
|
|
5
|
+
import { readdir, realpath, rename, rmdir, unlink } from 'node:fs/promises'
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import {
|
|
8
8
|
display,
|
|
@@ -110,10 +110,29 @@ export async function removeSkill({ slug } = {}, root = writeRoot()) {
|
|
|
110
110
|
return { slug, removed: gone, path: display(dir), kept: left.length ? left : undefined }
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Rename a skill: its folder moved whole (scripts, references and all) from `from` to `to`, then its
|
|
115
|
+
* SKILL.md rewritten there. Both names stay inside the folder, neither may be a link, and a skill
|
|
116
|
+
* already called `to` is never replaced.
|
|
117
|
+
*/
|
|
118
|
+
export async function renameSkill({ from, to, md } = {}, root = writeRoot()) {
|
|
119
|
+
const [was, dir] = [inside(root, from, WHAT), inside(root, to, WHAT)]
|
|
120
|
+
const text = String(md ?? '')
|
|
121
|
+
if (!/^---\r?\n/.test(text)) throw new Error('A SKILL.md starts with front matter between --- lines')
|
|
122
|
+
if (Buffer.byteLength(text) > MAX_MD) throw new Error('That SKILL.md is too large')
|
|
123
|
+
await refuseLinks([root, was, dir], 'rename')
|
|
124
|
+
if (!(await exists(join(was, 'SKILL.md')))) throw new Error(`No skill in ${display(was)}`)
|
|
125
|
+
if (await exists(dir)) throw new Error(`A skill named ${to} is already on your Mac`)
|
|
126
|
+
await rename(was, dir)
|
|
127
|
+
await writeWhole(join(dir, 'SKILL.md'), text)
|
|
128
|
+
return { slug: to, ...skillSummary(text, to), path: display(dir), providers: ['claude'], from: display(was) }
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
/** The bridge tools (index.mjs). */
|
|
114
132
|
export const skillTools = {
|
|
115
133
|
list_skills: () => listSkills(),
|
|
116
134
|
read_skill: args => readSkill(args),
|
|
117
135
|
write_skill: args => writeSkill(args),
|
|
118
136
|
remove_skill: args => removeSkill(args),
|
|
137
|
+
rename_skill: args => renameSkill(args),
|
|
119
138
|
}
|