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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kira-glasses-bridge",
3
- "version": "2.27.0",
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": [
@@ -1,10 +1,12 @@
1
- // Subagents on this Mac: Claude Code's (one Markdown file each in ~/.claude/agents, front matter
2
- // for its settings and the body as its system prompt, with `memory: user` kept in
3
- // ~/.claude/agent-memory/<name>/MEMORY.md) and Codex's (~/.codex/agents/<name>.toml, listed only:
4
- // the bridge doesn't run Codex yet). Kira's agents are Claude Code subagents: she writes their
5
- // files and memory here, and a task runs as one (agents/claude.mjs).
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
- /** A TOML string value on its own line (`name = "x"`), enough for Codex's name and description. */
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
- const m = new RegExp(`^${key}\\s*=\\s*(?:"((?:[^"\\\\]|\\\\.)*)"|'([^']*)')`, 'm').exec(toml)
37
- return m ? (m[1] ?? m[2]).replace(/\\"/g, '"') : null
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
- /** One of Claude Code's subagents, with its memory. */
82
- export async function readAgent({ slug } = {}, root = claudeRoot(), memoryRoot = FOLDERS.claude.agentMemory()) {
83
- const file = inside(root, slug, WHAT, { ...NAMED, ext: '.md' })
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
- return { slug, ...summary, path: display(file), md, memory: await readMemory(summary.name, memoryRoot) }
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 Claude Code subagent's file, whole: replacing the one there, or with `create` only a new
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 = claudeRoot()) {
95
- const file = inside(root, slug, WHAT, { ...NAMED, ext: '.md' })
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
- if (!/^---\r?\n/.test(text)) throw new Error('A subagent file starts with front matter between --- lines')
128
+ kind.check(text)
98
129
  if (Buffer.byteLength(text) > MAX_MD) throw new Error('That subagent file is too large')
99
- await refuseLinks([root, file], 'edit')
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
- return { slug, ...agentSummary(text, slug), path: display(file), providers: ['claude'] }
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 Claude Code subagent's file. Its memory folder stays (it's what it learned). */
106
- export async function removeAgent({ slug } = {}, root = claudeRoot()) {
107
- const file = inside(root, slug, WHAT, { ...NAMED, ext: '.md' })
108
- await refuseLinks([root, file], 'remove')
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
+ }
@@ -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
- const config = persona?.kiraTools ? kiraServer(persona.kiraTools) : undefined
115
- const instructions = persona?.systemPrompt ? { developerInstructions: persona.systemPrompt } : {}
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 })
@@ -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 only Claude Code's. */
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' },
@@ -1,8 +1,10 @@
1
- // Kira's built-in skills as Claude Code takes skills that come with something: a plugin, on this
2
- // Mac (~/.kira/claude-plugin: .claude-plugin/plugin.json and skills/<name>/SKILL.md). The API sends
3
- // the ones a person's Kira has with each job she runs as Claude Code (persona.skills); this brings
4
- // the folder in line with them (each written whole, the ones gone removed) and Claude Code loads it
5
- // with the job (the Agent SDK's `plugins`), so it reads them as its own: /kira:draft-page.
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 query options that load Kira's skills for a job: none when it has none, or when the folder
51
- * can't be written (the job still runs; `warn` says why).
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
- export async function kiraPluginOptions(persona, warn = log, root = pluginRoot()) {
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 { plugins: [{ type: 'local', path: await writeKiraPlugin(persona.skills, root) }] }
58
+ return await writeKiraPlugin(persona.skills, root)
57
59
  } catch (err) {
58
- warn(`Kira's skills couldn't be written for Claude Code: ${err.message}`)
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
+ }