kira-glasses-bridge 2.22.0 → 2.23.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/claude-config.mjs +153 -0
- package/src/index.mjs +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Claude Code's own skills on this Mac: one SKILL.md each in ~/.claude/skills/<slug>/ (or under
|
|
2
|
+
// $CLAUDE_CONFIG_DIR), which Claude Code and the bridge's Agent SDK jobs load natively. Kira lists
|
|
3
|
+
// all of them (hers and any other), and writes and removes the ones a person makes in Kira. Every
|
|
4
|
+
// path is confined to that folder: a slug is one plain folder name, and a symlinked folder is never
|
|
5
|
+
// written through.
|
|
6
|
+
import { lstat, mkdir, readdir, readFile, rename, rmdir, stat, unlink, writeFile } from 'node:fs/promises'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join, relative } from 'node:path'
|
|
9
|
+
|
|
10
|
+
const SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/
|
|
11
|
+
/** A SKILL.md past this isn't one: skipped when listing, refused when writing. */
|
|
12
|
+
const MAX_MD = 256 * 1024
|
|
13
|
+
|
|
14
|
+
/** Where Claude Code keeps personal skills. */
|
|
15
|
+
function skillsRoot() {
|
|
16
|
+
return join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'), 'skills')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The folder for a skill, after checking its slug can't name anything else. */
|
|
20
|
+
function folder(root, slug) {
|
|
21
|
+
const s = String(slug ?? '')
|
|
22
|
+
if (!SLUG.test(s)) throw new Error(`“${s}” isn’t a skill folder name (lowercase letters, digits and dashes)`)
|
|
23
|
+
return join(root, s)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** As people read it: under the home folder with ~. */
|
|
27
|
+
function display(full) {
|
|
28
|
+
const rel = relative(homedir(), full)
|
|
29
|
+
return rel && !rel.startsWith('..') ? `~/${rel}` : full
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A front-matter value: quoted or plain on its line, or a `|` / `>` block below it. */
|
|
33
|
+
function field(front, key) {
|
|
34
|
+
const lines = front.split('\n')
|
|
35
|
+
const at = lines.findIndex(l => l.startsWith(`${key}:`))
|
|
36
|
+
if (at < 0) return null
|
|
37
|
+
const raw = lines[at].slice(key.length + 1).trim()
|
|
38
|
+
if (/^[|>][+-]?$/.test(raw)) {
|
|
39
|
+
const block = []
|
|
40
|
+
for (const l of lines.slice(at + 1)) {
|
|
41
|
+
if (l.trim() && !/^\s/.test(l)) break
|
|
42
|
+
block.push(l.trim())
|
|
43
|
+
}
|
|
44
|
+
return block.join(raw.startsWith('|') ? '\n' : ' ').trim() || null
|
|
45
|
+
}
|
|
46
|
+
const quoted = /^(["'])(.*)\1$/.exec(raw)
|
|
47
|
+
return (quoted ? quoted[2] : raw.replace(/\s+#.*$/, '')).trim() || null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The name and description Claude Code shows, from a SKILL.md (the folder's name when it has none). */
|
|
51
|
+
export function skillSummary(md, slug) {
|
|
52
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md)
|
|
53
|
+
const front = m ? m[1].replace(/\r\n/g, '\n') : ''
|
|
54
|
+
const body = m ? md.slice(m[0].length) : md
|
|
55
|
+
const firstLine = body.split('\n').map(l => l.trim()).find(Boolean) ?? ''
|
|
56
|
+
return { name: field(front, 'name') || slug, description: field(front, 'description') || firstLine }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function isSymlink(p) {
|
|
60
|
+
return lstat(p).then(
|
|
61
|
+
s => s.isSymbolicLink(),
|
|
62
|
+
() => false,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A folder that links elsewhere is changed where it really is, never through the link. */
|
|
67
|
+
async function refuseLinks(root, dir, verb) {
|
|
68
|
+
if ((await isSymlink(root)) || (await isSymlink(dir))) {
|
|
69
|
+
throw new Error(`${display(dir)} is a link to somewhere else; ${verb} it there`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Every skill in the folder: its slug, name, description, where it is, and its SKILL.md. */
|
|
74
|
+
export async function listSkills(root = skillsRoot()) {
|
|
75
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(e => {
|
|
76
|
+
if (e.code === 'ENOENT') return []
|
|
77
|
+
throw e
|
|
78
|
+
})
|
|
79
|
+
const skills = []
|
|
80
|
+
for (const e of entries) {
|
|
81
|
+
if (!SLUG.test(e.name) || !(e.isDirectory() || e.isSymbolicLink())) continue
|
|
82
|
+
const file = join(root, e.name, 'SKILL.md')
|
|
83
|
+
const st = await stat(file).catch(() => null)
|
|
84
|
+
if (!st?.isFile() || st.size > MAX_MD) continue
|
|
85
|
+
const md = await readFile(file, 'utf8').catch(() => null)
|
|
86
|
+
if (md === null) continue
|
|
87
|
+
skills.push({ slug: e.name, ...skillSummary(md, e.name), path: display(join(root, e.name)), md })
|
|
88
|
+
}
|
|
89
|
+
skills.sort((a, b) => a.slug.localeCompare(b.slug))
|
|
90
|
+
return { root: display(root), skills }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** One skill's SKILL.md. */
|
|
94
|
+
export async function readSkill({ slug } = {}, root = skillsRoot()) {
|
|
95
|
+
const dir = folder(root, slug)
|
|
96
|
+
const md = await readFile(join(dir, 'SKILL.md'), 'utf8').catch(() => null)
|
|
97
|
+
if (md === null) return { error: `No skill in ${display(dir)}` }
|
|
98
|
+
return { slug, ...skillSummary(md, slug), path: display(dir), md }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Write a skill's SKILL.md, whole: replacing the one there, or with `create` only a new one (a skill
|
|
103
|
+
* already in that folder, Kira's or not, is never overwritten by a new one of the same name).
|
|
104
|
+
*/
|
|
105
|
+
export async function writeSkill({ slug, md, create = false } = {}, root = skillsRoot()) {
|
|
106
|
+
const dir = folder(root, slug)
|
|
107
|
+
const text = String(md ?? '')
|
|
108
|
+
if (!/^---\r?\n/.test(text)) throw new Error('A SKILL.md starts with front matter between --- lines')
|
|
109
|
+
if (Buffer.byteLength(text) > MAX_MD) throw new Error('That SKILL.md is too large')
|
|
110
|
+
await refuseLinks(root, dir, 'edit')
|
|
111
|
+
const file = join(dir, 'SKILL.md')
|
|
112
|
+
if (create && (await stat(file).catch(() => null))) throw new Error(`A skill named ${slug} is already on your Mac`)
|
|
113
|
+
await mkdir(dir, { recursive: true })
|
|
114
|
+
const tmp = join(dir, `.SKILL.md.${process.pid}.tmp`)
|
|
115
|
+
await writeFile(tmp, text, 'utf8')
|
|
116
|
+
await rename(tmp, file)
|
|
117
|
+
return { slug, ...skillSummary(text, slug), path: display(dir) }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove a skill: its SKILL.md goes (so Claude Code stops loading it), and the folder too when
|
|
122
|
+
* nothing else is in it. Anything else a skill folder holds (scripts, references) stays.
|
|
123
|
+
*/
|
|
124
|
+
export async function removeSkill({ slug } = {}, root = skillsRoot()) {
|
|
125
|
+
const dir = folder(root, slug)
|
|
126
|
+
await refuseLinks(root, dir, 'remove')
|
|
127
|
+
const gone = await unlink(join(dir, 'SKILL.md')).then(
|
|
128
|
+
() => true,
|
|
129
|
+
e => {
|
|
130
|
+
if (e.code === 'ENOENT') return false
|
|
131
|
+
throw e
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
const left = await readdir(dir).catch(() => [])
|
|
135
|
+
if (!left.length) await rmdir(dir).catch(() => {})
|
|
136
|
+
return { slug, removed: gone, path: display(dir), kept: left.length ? left : undefined }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The bridge tools (index.mjs). */
|
|
140
|
+
export const skillTools = {
|
|
141
|
+
async list_skills() {
|
|
142
|
+
return listSkills()
|
|
143
|
+
},
|
|
144
|
+
async read_skill(args) {
|
|
145
|
+
return readSkill(args)
|
|
146
|
+
},
|
|
147
|
+
async write_skill(args) {
|
|
148
|
+
return writeSkill(args)
|
|
149
|
+
},
|
|
150
|
+
async remove_skill(args) {
|
|
151
|
+
return removeSkill(args)
|
|
152
|
+
},
|
|
153
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
5
|
import { AGENTS } from './agents/index.mjs'
|
|
6
6
|
import { claudeTools } from './agents/claude-local.mjs'
|
|
7
|
+
import { skillTools } from './claude-config.mjs'
|
|
7
8
|
import { connect, disconnect, sendIfLive } from './connection.mjs'
|
|
8
9
|
import { fileTools } from './file-tools.mjs'
|
|
9
10
|
import { gitTools } from './git-tools.mjs'
|
|
@@ -22,6 +23,7 @@ const tools = {
|
|
|
22
23
|
...jobTools,
|
|
23
24
|
...gitTools,
|
|
24
25
|
...claudeTools,
|
|
26
|
+
...skillTools,
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** What this bridge build implements, told to the API after its hello. */
|