kira-glasses-bridge 2.27.0 → 2.28.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 +2 -1
- package/src/agent-files.mjs +59 -25
- package/src/agents/codex-agent.mjs +47 -0
- package/src/agents/codex.mjs +10 -2
- package/src/config-folders.mjs +1 -1
- package/src/kira-plugin.mjs +26 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kira-glasses-bridge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.28.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": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@anthropic-ai/claude-agent-sdk": "^0.3.278",
|
|
22
|
+
"smol-toml": "1.9.0",
|
|
22
23
|
"ws": "^8.18.0"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
package/src/agent-files.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
// Subagents on this Mac: Claude Code's (one Markdown file each in
|
|
2
|
-
// for its settings and the body as its system prompt, with
|
|
3
|
-
// ~/.claude/agent-memory/<name>/MEMORY.md) and Codex's (
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Subagents on this Mac, in each agent's own format: Claude Code's (one Markdown file each in
|
|
2
|
+
// ~/.claude/agents, front matter for its settings and the body as its system prompt, with
|
|
3
|
+
// `memory: user` kept in ~/.claude/agent-memory/<name>/MEMORY.md) and Codex's (one TOML file each
|
|
4
|
+
// in ~/.codex/agents: name, description, developer_instructions and any config key; Codex keeps no
|
|
5
|
+
// memory per agent). Kira's agents are one or the other: she writes their files (and a Claude Code
|
|
6
|
+
// one's memory) here, and a task runs as one (agents/claude.mjs, agents/codex-agent.mjs).
|
|
6
7
|
import { readdir, unlink } from 'node:fs/promises'
|
|
7
8
|
import { join } from 'node:path'
|
|
9
|
+
import { parse } from 'smol-toml'
|
|
8
10
|
import {
|
|
9
11
|
FOLDERS,
|
|
10
12
|
agentFolders,
|
|
@@ -29,12 +31,34 @@ const LOAD_BYTES = 25 * 1024
|
|
|
29
31
|
const KIRAS_LINE = /^- \d{4}-\d{2}-\d{2}: /
|
|
30
32
|
const WHAT = 'a subagent'
|
|
31
33
|
const NAMED = { loose: true }
|
|
32
|
-
const claudeRoot = () => agentFolders()[0].root
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
+
/** Each agent's folder and file: where Kira writes it, and what a file of it must be. */
|
|
36
|
+
const KINDS = {
|
|
37
|
+
claude: { root: () => agentFolders()[0].root, ext: '.md', check: checkMd },
|
|
38
|
+
codex: { root: () => agentFolders()[1].root, ext: '.toml', check: checkToml },
|
|
39
|
+
}
|
|
40
|
+
const kindOf = provider => KINDS[provider ?? 'claude'] ?? KINDS.claude
|
|
41
|
+
|
|
42
|
+
function checkMd(text) {
|
|
43
|
+
if (!/^---\r?\n/.test(text)) throw new Error('A subagent file starts with front matter between --- lines')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function checkToml(text) {
|
|
47
|
+
try {
|
|
48
|
+
parse(text)
|
|
49
|
+
} catch (err) {
|
|
50
|
+
throw new Error(`A Codex agent file is TOML: ${err.message}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A Codex agent file's string field, or null (and null for all when it isn't TOML). */
|
|
35
55
|
const tomlField = (toml, key) => {
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
try {
|
|
57
|
+
const v = parse(toml)[key]
|
|
58
|
+
return typeof v === 'string' ? v : null
|
|
59
|
+
} catch {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
38
62
|
}
|
|
39
63
|
|
|
40
64
|
/** The name and description a tool shows, from a subagent file (its file name when it has none). */
|
|
@@ -78,34 +102,44 @@ async function readMemory(name, root) {
|
|
|
78
102
|
}
|
|
79
103
|
}
|
|
80
104
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
/**
|
|
106
|
+
* One subagent, `provider`'s (Claude Code's by default), with its memory (Claude Code's only). `md`
|
|
107
|
+
* is its file's text whatever the format: Markdown for Claude Code, TOML for Codex.
|
|
108
|
+
*/
|
|
109
|
+
export async function readAgent({ slug, provider } = {}, root, memoryRoot = FOLDERS.claude.agentMemory()) {
|
|
110
|
+
const kind = kindOf(provider)
|
|
111
|
+
const file = inside(root ?? kind.root(), slug, WHAT, { ...NAMED, ext: kind.ext })
|
|
84
112
|
const md = await readSmall(file, MAX_MD)
|
|
85
113
|
if (md === null) return { error: `No subagent at ${display(file)}` }
|
|
86
|
-
const summary = agentSummary(md, slug)
|
|
87
|
-
|
|
114
|
+
const summary = agentSummary(md, slug, kind.ext)
|
|
115
|
+
const memory = kind === KINDS.claude ? await readMemory(summary.name, memoryRoot) : ''
|
|
116
|
+
return { slug, ...summary, path: display(file), md, memory }
|
|
88
117
|
}
|
|
89
118
|
|
|
90
119
|
/**
|
|
91
|
-
* Write a
|
|
92
|
-
* one (a subagent already there, Kira's or not, is never overwritten by a new one of that name).
|
|
120
|
+
* Write a subagent's file (`provider`'s), whole: replacing the one there, or with `create` only a
|
|
121
|
+
* new one (a subagent already there, Kira's or not, is never overwritten by a new one of that name).
|
|
93
122
|
*/
|
|
94
|
-
export async function writeAgent({ slug, md, create = false } = {}, root
|
|
95
|
-
const
|
|
123
|
+
export async function writeAgent({ slug, md, create = false, provider } = {}, root) {
|
|
124
|
+
const kind = kindOf(provider)
|
|
125
|
+
const dir = root ?? kind.root()
|
|
126
|
+
const file = inside(dir, slug, WHAT, { ...NAMED, ext: kind.ext })
|
|
96
127
|
const text = String(md ?? '')
|
|
97
|
-
|
|
128
|
+
kind.check(text)
|
|
98
129
|
if (Buffer.byteLength(text) > MAX_MD) throw new Error('That subagent file is too large')
|
|
99
|
-
await refuseLinks([
|
|
130
|
+
await refuseLinks([dir, file], 'edit')
|
|
100
131
|
if (create && (await exists(file))) throw new Error(`A subagent named ${slug} is already on your Mac`)
|
|
101
132
|
await writeWhole(file, text)
|
|
102
|
-
|
|
133
|
+
const providers = [kind === KINDS.codex ? 'codex' : 'claude']
|
|
134
|
+
return { slug, ...agentSummary(text, slug, kind.ext), path: display(file), providers }
|
|
103
135
|
}
|
|
104
136
|
|
|
105
|
-
/** Remove a
|
|
106
|
-
export async function removeAgent({ slug } = {}, root
|
|
107
|
-
const
|
|
108
|
-
|
|
137
|
+
/** Remove a subagent's file (`provider`'s). A Claude Code one's memory folder stays (it's what it learned). */
|
|
138
|
+
export async function removeAgent({ slug, provider } = {}, root) {
|
|
139
|
+
const kind = kindOf(provider)
|
|
140
|
+
const dir = root ?? kind.root()
|
|
141
|
+
const file = inside(dir, slug, WHAT, { ...NAMED, ext: kind.ext })
|
|
142
|
+
await refuseLinks([dir, file], 'remove')
|
|
109
143
|
const removed = await unlink(file).then(
|
|
110
144
|
() => true,
|
|
111
145
|
e => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// A Kira agent that runs on Codex: its Codex agent file (~/.codex/agents/<handle>.toml, written by
|
|
2
|
+
// agent-files.mjs). Codex's app server has no "start this thread as agent X" (its agent roles are a
|
|
3
|
+
// config layer it applies when it spawns a subagent: codex-rs/agent-roles), so a job applies the
|
|
4
|
+
// file the way Codex applies a role: its `developer_instructions` are the thread's developer
|
|
5
|
+
// instructions (what Kira adds for the run after them) and its other settings (model,
|
|
6
|
+
// model_reasoning_effort…) the thread's config. Fields from codex-rs/agent-roles/src/agent_role_config.rs.
|
|
7
|
+
import { parse } from 'smol-toml'
|
|
8
|
+
import { FOLDERS, display, inside, readSmall } from '../config-folders.mjs'
|
|
9
|
+
|
|
10
|
+
const MAX_TOML = 256 * 1024
|
|
11
|
+
/** What names and describes the agent: Codex reads them for its spawn list, never as config. */
|
|
12
|
+
const NOT_CONFIG = new Set(['name', 'description', 'nickname_candidates', 'developer_instructions'])
|
|
13
|
+
|
|
14
|
+
/** A table's settings as Codex's config overrides take them: one dotted key per value. */
|
|
15
|
+
function flatten(table, prefix = '', out = {}) {
|
|
16
|
+
for (const [k, v] of Object.entries(table)) {
|
|
17
|
+
const key = prefix + k
|
|
18
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date)) flatten(v, `${key}.`, out)
|
|
19
|
+
else out[key] = v instanceof Date ? v.toISOString() : v
|
|
20
|
+
}
|
|
21
|
+
return out
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** An agent file as a thread takes it: its instructions and the rest of its config. Throws when it isn't TOML. */
|
|
25
|
+
export function roleLayer(toml) {
|
|
26
|
+
const table = parse(String(toml ?? ''))
|
|
27
|
+
const rest = Object.fromEntries(Object.entries(table).filter(([k]) => !NOT_CONFIG.has(k)))
|
|
28
|
+
const said = table.developer_instructions
|
|
29
|
+
return { instructions: typeof said === 'string' ? said.trim() : '', config: flatten(rest) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The thread's developer instructions: the agent's own, then what Kira adds for this run. */
|
|
33
|
+
export const developerInstructions = (role, persona) =>
|
|
34
|
+
[role?.instructions, persona?.systemPrompt].filter(Boolean).join('\n\n')
|
|
35
|
+
|
|
36
|
+
/** The Codex agent a job runs as (null for none); throws, saying where, when its file isn't there. */
|
|
37
|
+
export async function codexAgentOf(persona, root = FOLDERS.codex.agents()) {
|
|
38
|
+
if (!persona?.agent) return null
|
|
39
|
+
const file = inside(root, persona.agent, 'a Codex agent', { loose: true, ext: '.toml' })
|
|
40
|
+
const toml = await readSmall(file, MAX_TOML)
|
|
41
|
+
if (toml === null) throw new Error(`@${persona.agent}'s Codex agent isn't on this Mac (${display(file)})`)
|
|
42
|
+
try {
|
|
43
|
+
return roleLayer(toml)
|
|
44
|
+
} catch (err) {
|
|
45
|
+
throw new Error(`${display(file)} isn't TOML Codex can read: ${err.message}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/agents/codex.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import { TEXT_CAP, short } from './claude-calls.mjs'
|
|
|
6
6
|
import { callsOf, codexOptionsFor, itemAction, itemResult, strictest } from './codex-items.mjs'
|
|
7
7
|
import { codexBin, openCodex } from './codex-rpc.mjs'
|
|
8
8
|
import { kiraDecides } from './checks.mjs'
|
|
9
|
+
import { codexAgentOf, developerInstructions } from './codex-agent.mjs'
|
|
10
|
+
import { kiraSkillRoots } from '../kira-plugin.mjs'
|
|
9
11
|
|
|
10
12
|
const COMMAND = 'item/commandExecution/requestApproval'
|
|
11
13
|
const APPROVALS = new Set([COMMAND, 'item/fileChange/requestApproval'])
|
|
@@ -111,8 +113,14 @@ async function run({ prompt, sessionId, cwd, persona, settings, abort }, io) {
|
|
|
111
113
|
try {
|
|
112
114
|
await codex.request('initialize', { clientInfo: { name: 'kira', title: 'Kira', version: '1' }, capabilities: null })
|
|
113
115
|
codex.notify('initialized')
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
+
// A Kira agent on Codex: its agent file, applied as Codex applies a role (codex-agent.mjs).
|
|
117
|
+
const role = await codexAgentOf(persona)
|
|
118
|
+
// Kira's built-in skills, as Codex's own for this app server: the folder Claude Code's plugin uses.
|
|
119
|
+
const roots = await kiraSkillRoots(persona)
|
|
120
|
+
if (roots.length) await codex.request('skills/extraRoots/set', { extraRoots: roots })
|
|
121
|
+
const config = { ...role?.config, ...(persona?.kiraTools ? kiraServer(persona.kiraTools) : {}) }
|
|
122
|
+
const said = developerInstructions(role, persona)
|
|
123
|
+
const instructions = said ? { developerInstructions: said } : {}
|
|
116
124
|
const opened = thread
|
|
117
125
|
? await codex.request('thread/resume', { threadId: thread, cwd, ...options, ...instructions, config })
|
|
118
126
|
: await codex.request('thread/start', { cwd, ...options, ...instructions, config })
|
package/src/config-folders.mjs
CHANGED
|
@@ -38,7 +38,7 @@ export const skillFolders = () => [
|
|
|
38
38
|
{ root: FOLDERS.codex.skills(), loadedBy: ['codex'] },
|
|
39
39
|
]
|
|
40
40
|
|
|
41
|
-
/** The subagent folders, their file format and who loads each; Kira writes
|
|
41
|
+
/** The subagent folders, their file format and who loads each; Kira writes both (agent-files.mjs). */
|
|
42
42
|
export const agentFolders = () => [
|
|
43
43
|
{ root: FOLDERS.claude.agents(), loadedBy: ['claude'], ext: '.md' },
|
|
44
44
|
{ root: FOLDERS.codex.agents(), loadedBy: ['codex'], ext: '.toml' },
|
package/src/kira-plugin.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
// Kira's built-in skills
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// with the job (the Agent SDK's `plugins
|
|
1
|
+
// Kira's built-in skills, one folder on this Mac that both agents load as their own. The API sends
|
|
2
|
+
// the ones a person's Kira has with each job (persona.skills); this brings the folder in line with
|
|
3
|
+
// them (each written whole, the ones gone removed). It is a Claude Code plugin
|
|
4
|
+
// (~/.kira/claude-plugin: .claude-plugin/plugin.json and skills/<name>/SKILL.md), which Claude Code
|
|
5
|
+
// loads with the job (the Agent SDK's `plugins`: /kira:draft-page). Its skills/ folder is plain
|
|
6
|
+
// Agent Skills (<name>/SKILL.md), which Codex loads as an extra skills root for the job
|
|
7
|
+
// (`skills/extraRoots/set` on its app server: $draft-page).
|
|
6
8
|
import { readdir, rm } from 'node:fs/promises'
|
|
7
9
|
import { join } from 'node:path'
|
|
8
10
|
import { KIRA_DIR } from './config.mjs'
|
|
@@ -47,15 +49,27 @@ export function writeKiraPlugin(skills, root = pluginRoot()) {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
|
-
* The
|
|
51
|
-
*
|
|
52
|
+
* The folder written for a job's skills, or null when it has none or it can't be written (the job
|
|
53
|
+
* still runs; `warn` says why, naming the agent that goes without them).
|
|
52
54
|
*/
|
|
53
|
-
|
|
54
|
-
if (!Array.isArray(persona?.skills) || !persona.skills.length) return
|
|
55
|
+
async function writtenFor(persona, agent, warn, root) {
|
|
56
|
+
if (!Array.isArray(persona?.skills) || !persona.skills.length) return null
|
|
55
57
|
try {
|
|
56
|
-
return
|
|
58
|
+
return await writeKiraPlugin(persona.skills, root)
|
|
57
59
|
} catch (err) {
|
|
58
|
-
warn(`Kira's skills couldn't be written for
|
|
59
|
-
return
|
|
60
|
+
warn(`Kira's skills couldn't be written for ${agent}: ${err.message}`)
|
|
61
|
+
return null
|
|
60
62
|
}
|
|
61
63
|
}
|
|
64
|
+
|
|
65
|
+
/** The Agent SDK query options that load Kira's skills for a Claude Code job (none without them). */
|
|
66
|
+
export async function kiraPluginOptions(persona, warn = log, root = pluginRoot()) {
|
|
67
|
+
const path = await writtenFor(persona, 'Claude Code', warn, root)
|
|
68
|
+
return path ? { plugins: [{ type: 'local', path }] } : {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The skills roots Codex is given for a job (`skills/extraRoots/set`): the plugin's skills/ folder, or none. */
|
|
72
|
+
export async function kiraSkillRoots(persona, warn = log, root = pluginRoot()) {
|
|
73
|
+
const path = await writtenFor(persona, 'Codex', warn, root)
|
|
74
|
+
return path ? [join(path, 'skills')] : []
|
|
75
|
+
}
|