blyuecode 0.3.8 → 0.5.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/dist/core/agent.js +13 -1
- package/dist/core/config.js +8 -0
- package/dist/core/skills.js +88 -0
- package/dist/core/skillstore.js +118 -0
- package/dist/ui/Chat.js +63 -1
- package/dist/ui/Settings.js +24 -0
- package/dist/ui/SkillStorePanel.js +145 -0
- package/dist/ui/SkillsPanel.js +20 -0
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Provider } from './provider.js';
|
|
2
2
|
import { getToolDefs, executeTool } from './tools.js';
|
|
3
3
|
import { loadSession, saveSession, clearSession } from './session.js';
|
|
4
|
+
import { formatSkillForPrompt } from './skills.js';
|
|
4
5
|
const SYSTEM_PROMPT = 'You are BLYUE, a coding assistant running in a terminal. ' +
|
|
5
6
|
'Use tools to read, write, and run commands. ' +
|
|
6
7
|
'If n8n tools are available, you can also manage n8n workflows. ' +
|
|
@@ -29,6 +30,16 @@ export class Agent {
|
|
|
29
30
|
}
|
|
30
31
|
catch { }
|
|
31
32
|
}
|
|
33
|
+
buildSystemMessage(skills) {
|
|
34
|
+
let content = SYSTEM_PROMPT;
|
|
35
|
+
if (skills.length > 0) {
|
|
36
|
+
content += '\n\n# Active skills\n';
|
|
37
|
+
for (const sk of skills) {
|
|
38
|
+
content += '\n' + formatSkillForPrompt(sk) + '\n';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { role: 'system', content };
|
|
42
|
+
}
|
|
32
43
|
historySize() {
|
|
33
44
|
return this.messages.length - 1;
|
|
34
45
|
}
|
|
@@ -63,7 +74,8 @@ export class Agent {
|
|
|
63
74
|
return null;
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
|
-
async *run(userInput) {
|
|
77
|
+
async *run(userInput, opts) {
|
|
78
|
+
this.messages[0] = this.buildSystemMessage(opts?.activeSkills ?? []);
|
|
67
79
|
this.messages.push({ role: 'user', content: userInput });
|
|
68
80
|
this.persist();
|
|
69
81
|
const compacted = await this.maybeAutoCompact();
|
package/dist/core/config.js
CHANGED
|
@@ -28,6 +28,7 @@ function parseEnv(file) {
|
|
|
28
28
|
model: map.MODEL || 'gpt-4o-mini',
|
|
29
29
|
n8nUrl: map.N8N_URL || undefined,
|
|
30
30
|
n8nApiKey: map.N8N_API_KEY || undefined,
|
|
31
|
+
skillsIndexUrl: map.SKILLS_INDEX_URL || undefined,
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
export function saveConfig(cfg) {
|
|
@@ -44,6 +45,8 @@ export function saveConfig(cfg) {
|
|
|
44
45
|
lines.push(`N8N_URL=${cfg.n8nUrl}`);
|
|
45
46
|
if (cfg.n8nApiKey)
|
|
46
47
|
lines.push(`N8N_API_KEY=${cfg.n8nApiKey}`);
|
|
48
|
+
if (cfg.skillsIndexUrl)
|
|
49
|
+
lines.push(`SKILLS_INDEX_URL=${cfg.skillsIndexUrl}`);
|
|
47
50
|
lines.push('');
|
|
48
51
|
fs.writeFileSync(GLOBAL_ENV, lines.join('\n'), { mode: 0o600 });
|
|
49
52
|
return GLOBAL_ENV;
|
|
@@ -63,6 +66,10 @@ export function applyConfig(cfg) {
|
|
|
63
66
|
process.env.N8N_API_KEY = cfg.n8nApiKey;
|
|
64
67
|
else
|
|
65
68
|
delete process.env.N8N_API_KEY;
|
|
69
|
+
if (cfg.skillsIndexUrl)
|
|
70
|
+
process.env.SKILLS_INDEX_URL = cfg.skillsIndexUrl;
|
|
71
|
+
else
|
|
72
|
+
delete process.env.SKILLS_INDEX_URL;
|
|
66
73
|
}
|
|
67
74
|
export function getConfig() {
|
|
68
75
|
const cfg = loadConfig();
|
|
@@ -74,6 +81,7 @@ export function getConfig() {
|
|
|
74
81
|
model: process.env.MODEL || 'gpt-4o-mini',
|
|
75
82
|
n8nUrl: process.env.N8N_URL,
|
|
76
83
|
n8nApiKey: process.env.N8N_API_KEY,
|
|
84
|
+
skillsIndexUrl: process.env.SKILLS_INDEX_URL,
|
|
77
85
|
};
|
|
78
86
|
}
|
|
79
87
|
export function hasN8n() {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const SKILLS_DIR = path.join(os.homedir(), '.blyue', 'skills');
|
|
5
|
+
export function skillsDir() {
|
|
6
|
+
return SKILLS_DIR;
|
|
7
|
+
}
|
|
8
|
+
export function ensureSkillsDir() {
|
|
9
|
+
if (!fs.existsSync(SKILLS_DIR)) {
|
|
10
|
+
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function stripQuotes(s) {
|
|
14
|
+
return s.replace(/^["']|["']$/g, '').trim();
|
|
15
|
+
}
|
|
16
|
+
function parseFrontmatter(raw) {
|
|
17
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
18
|
+
if (!m)
|
|
19
|
+
return null;
|
|
20
|
+
const [, front, body] = m;
|
|
21
|
+
const meta = {};
|
|
22
|
+
for (const line of front.split(/\r?\n/)) {
|
|
23
|
+
if (!line.trim() || line.trim().startsWith('#'))
|
|
24
|
+
continue;
|
|
25
|
+
const kv = line.match(/^(\w+):\s*(.*)$/);
|
|
26
|
+
if (!kv)
|
|
27
|
+
continue;
|
|
28
|
+
const [, key, rawVal] = kv;
|
|
29
|
+
if (key === 'triggers') {
|
|
30
|
+
const cleaned = rawVal.replace(/^\[|\]$/g, '');
|
|
31
|
+
meta.triggers = cleaned
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((s) => stripQuotes(s))
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
meta[key] = stripQuotes(rawVal);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { meta, body: body.trim() };
|
|
41
|
+
}
|
|
42
|
+
function loadOne(skillFile) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(skillFile, 'utf8');
|
|
45
|
+
const parsed = parseFrontmatter(raw);
|
|
46
|
+
if (!parsed)
|
|
47
|
+
return null;
|
|
48
|
+
const fallbackName = path.basename(path.dirname(skillFile));
|
|
49
|
+
return {
|
|
50
|
+
name: parsed.meta.name || fallbackName,
|
|
51
|
+
description: parsed.meta.description || '',
|
|
52
|
+
triggers: parsed.meta.triggers || [],
|
|
53
|
+
body: parsed.body,
|
|
54
|
+
path: skillFile,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function listSkills() {
|
|
62
|
+
if (!fs.existsSync(SKILLS_DIR))
|
|
63
|
+
return [];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const entry of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) {
|
|
66
|
+
if (!entry.isDirectory())
|
|
67
|
+
continue;
|
|
68
|
+
const skillFile = path.join(SKILLS_DIR, entry.name, 'SKILL.md');
|
|
69
|
+
if (!fs.existsSync(skillFile))
|
|
70
|
+
continue;
|
|
71
|
+
const skill = loadOne(skillFile);
|
|
72
|
+
if (skill)
|
|
73
|
+
out.push(skill);
|
|
74
|
+
}
|
|
75
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
76
|
+
}
|
|
77
|
+
export function findSkill(name) {
|
|
78
|
+
const lc = name.toLowerCase();
|
|
79
|
+
return (listSkills().find((s) => s.name.toLowerCase() === lc) || null);
|
|
80
|
+
}
|
|
81
|
+
export function matchSkills(userInput) {
|
|
82
|
+
const lc = userInput.toLowerCase();
|
|
83
|
+
return listSkills().filter((s) => s.triggers.some((t) => t && lc.includes(t.toLowerCase())));
|
|
84
|
+
}
|
|
85
|
+
export function formatSkillForPrompt(skill) {
|
|
86
|
+
const desc = skill.description ? ` — ${skill.description}` : '';
|
|
87
|
+
return `## Skill: ${skill.name}${desc}\n${skill.body}`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { skillsDir } from './skills.js';
|
|
4
|
+
const DEFAULT_INDEX_URL = 'https://raw.githubusercontent.com/lumenlabsllc/blyue-skills/main/index.json';
|
|
5
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
6
|
+
let cache = null;
|
|
7
|
+
export function indexUrl() {
|
|
8
|
+
return process.env.SKILLS_INDEX_URL || DEFAULT_INDEX_URL;
|
|
9
|
+
}
|
|
10
|
+
function baseUrlFor(url) {
|
|
11
|
+
return url.replace(/\/index\.json.*$/, '');
|
|
12
|
+
}
|
|
13
|
+
export async function fetchStore(opts) {
|
|
14
|
+
const url = indexUrl();
|
|
15
|
+
if (!opts?.force &&
|
|
16
|
+
cache &&
|
|
17
|
+
cache.url === url &&
|
|
18
|
+
Date.now() - cache.at < CACHE_TTL_MS) {
|
|
19
|
+
return cache.skills;
|
|
20
|
+
}
|
|
21
|
+
let res;
|
|
22
|
+
try {
|
|
23
|
+
res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
throw new Error(`Cannot reach skill store: ${err.message}. ` +
|
|
27
|
+
`Check SKILLS_INDEX_URL in /settings.`);
|
|
28
|
+
}
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
throw new Error(`Skill store returned HTTP ${res.status} ${res.statusText} — ${url}`);
|
|
31
|
+
}
|
|
32
|
+
const body = (await res.json());
|
|
33
|
+
if (!body || !Array.isArray(body.skills)) {
|
|
34
|
+
throw new Error('Skill store index is malformed (expected { version, skills: [...] })');
|
|
35
|
+
}
|
|
36
|
+
cache = { at: Date.now(), url, skills: body.skills };
|
|
37
|
+
return body.skills;
|
|
38
|
+
}
|
|
39
|
+
export function clearStoreCache() {
|
|
40
|
+
cache = null;
|
|
41
|
+
}
|
|
42
|
+
export function isInstalled(name) {
|
|
43
|
+
return fs.existsSync(path.join(skillsDir(), name, 'SKILL.md'));
|
|
44
|
+
}
|
|
45
|
+
export function installedVersion(name) {
|
|
46
|
+
const file = path.join(skillsDir(), name, 'SKILL.md');
|
|
47
|
+
if (!fs.existsSync(file))
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
51
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
52
|
+
if (!m)
|
|
53
|
+
return null;
|
|
54
|
+
const v = m[1].match(/^version:\s*["']?([^"'\r\n]+)["']?/m);
|
|
55
|
+
return v ? v[1].trim() : null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function installSkill(skill) {
|
|
62
|
+
const url = `${baseUrlFor(indexUrl())}/${skill.path.replace(/^\/+/, '')}`;
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await fetch(url, { headers: { Accept: 'text/plain' } });
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
throw new Error(`Download failed: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
throw new Error(`Download failed: HTTP ${res.status} for ${url}`);
|
|
72
|
+
}
|
|
73
|
+
const text = await res.text();
|
|
74
|
+
if (!text.trim().startsWith('---')) {
|
|
75
|
+
throw new Error(`Downloaded SKILL.md for "${skill.name}" has no frontmatter`);
|
|
76
|
+
}
|
|
77
|
+
const dir = path.join(skillsDir(), skill.name);
|
|
78
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
79
|
+
let out = text;
|
|
80
|
+
if (skill.version && !/^version:/m.test(text.split('---')[1] || '')) {
|
|
81
|
+
out = text.replace(/^---\r?\n([\s\S]*?)\r?\n---/, (_m, inner) => `---\n${inner}\nversion: ${skill.version}\n---`);
|
|
82
|
+
}
|
|
83
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), out, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
export function uninstallSkill(name) {
|
|
86
|
+
const dir = path.join(skillsDir(), name);
|
|
87
|
+
if (!fs.existsSync(dir))
|
|
88
|
+
return;
|
|
89
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
export async function updateAll() {
|
|
92
|
+
const store = await fetchStore();
|
|
93
|
+
const results = [];
|
|
94
|
+
for (const skill of store) {
|
|
95
|
+
if (!isInstalled(skill.name))
|
|
96
|
+
continue;
|
|
97
|
+
const from = installedVersion(skill.name);
|
|
98
|
+
const to = skill.version ?? null;
|
|
99
|
+
if (from && to && from === to) {
|
|
100
|
+
results.push({ name: skill.name, from, to, ok: true });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
await installSkill(skill);
|
|
105
|
+
results.push({ name: skill.name, from, to, ok: true });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
results.push({
|
|
109
|
+
name: skill.name,
|
|
110
|
+
from,
|
|
111
|
+
to,
|
|
112
|
+
ok: false,
|
|
113
|
+
error: err.message,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return results;
|
|
118
|
+
}
|
package/dist/ui/Chat.js
CHANGED
|
@@ -19,6 +19,9 @@ import { bumpActivity } from './activity.js';
|
|
|
19
19
|
import { theme } from './theme.js';
|
|
20
20
|
import { getConfig, saveConfig, applyConfig } from '../core/config.js';
|
|
21
21
|
import { recordModelUse } from '../core/modelUsage.js';
|
|
22
|
+
import { SkillsPanel } from './SkillsPanel.js';
|
|
23
|
+
import { SkillStorePanel } from './SkillStorePanel.js';
|
|
24
|
+
import { listSkills, findSkill, matchSkills, ensureSkillsDir, } from '../core/skills.js';
|
|
22
25
|
let nextId = 0;
|
|
23
26
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
24
27
|
const HistoryLine = ({ item }) => {
|
|
@@ -67,6 +70,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
67
70
|
const [loading, setLoading] = useState(false);
|
|
68
71
|
const [streaming, setStreaming] = useState('');
|
|
69
72
|
const [overlay, setOverlay] = useState('none');
|
|
73
|
+
const [pendingSkill, setPendingSkill] = useState(null);
|
|
70
74
|
const [permReq, setPermReq] = useState(null);
|
|
71
75
|
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
72
76
|
const [agent, setAgent] = useState(() => new Agent());
|
|
@@ -105,6 +109,10 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
105
109
|
if (key.ctrl && inputChar === 'o')
|
|
106
110
|
setOverlay('settings');
|
|
107
111
|
});
|
|
112
|
+
useEffect(() => { try {
|
|
113
|
+
ensureSkillsDir();
|
|
114
|
+
}
|
|
115
|
+
catch { } }, []);
|
|
108
116
|
const flush = (kind, text) => {
|
|
109
117
|
if (!text)
|
|
110
118
|
return;
|
|
@@ -130,6 +138,36 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
130
138
|
setOverlay('mcp');
|
|
131
139
|
return;
|
|
132
140
|
}
|
|
141
|
+
if (trimmed === '/skillstore') {
|
|
142
|
+
setInput('');
|
|
143
|
+
setOverlay('skillstore');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (trimmed === '/skills') {
|
|
147
|
+
setInput('');
|
|
148
|
+
setOverlay('skills');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (trimmed.startsWith('/skill ')) {
|
|
152
|
+
setInput('');
|
|
153
|
+
const name = trimmed.slice(7).trim();
|
|
154
|
+
const sk = findSkill(name);
|
|
155
|
+
if (!sk) {
|
|
156
|
+
const all = listSkills().map((x) => x.name).join(', ') || '(none)';
|
|
157
|
+
setHistory((h) => [
|
|
158
|
+
...h,
|
|
159
|
+
mkItem('error', 'unknown skill: ' + name),
|
|
160
|
+
mkItem('tool', 'available: ' + all),
|
|
161
|
+
]);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
setPendingSkill(sk);
|
|
165
|
+
setHistory((h) => [
|
|
166
|
+
...h,
|
|
167
|
+
mkItem('tool', 'skill armed: ' + sk.name + ' (applies to next message)'),
|
|
168
|
+
]);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
133
171
|
if (trimmed === '/debug') {
|
|
134
172
|
setInput('');
|
|
135
173
|
const lines = readDebug();
|
|
@@ -176,6 +214,9 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
176
214
|
mkItem('tool', ' /model switch model (Ctrl+L)'),
|
|
177
215
|
mkItem('tool', ' /settings open settings (Ctrl+O)'),
|
|
178
216
|
mkItem('tool', ' /mcp show MCP servers and tools'),
|
|
217
|
+
mkItem('tool', ' /skills list skills'),
|
|
218
|
+
mkItem('tool', ' /skillstore browse and install community skills'),
|
|
219
|
+
mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
|
|
179
220
|
mkItem('tool', ' /compact summarize history to free context'),
|
|
180
221
|
mkItem('tool', ' /debug show last request to provider'),
|
|
181
222
|
mkItem('tool', ' /clear clear session and permissions'),
|
|
@@ -191,7 +232,22 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
191
232
|
let textBuf = '';
|
|
192
233
|
let toolBuf = '';
|
|
193
234
|
try {
|
|
194
|
-
|
|
235
|
+
const matched = matchSkills(trimmed);
|
|
236
|
+
const activeSkills = [];
|
|
237
|
+
if (pendingSkill)
|
|
238
|
+
activeSkills.push(pendingSkill);
|
|
239
|
+
for (const m of matched) {
|
|
240
|
+
if (!activeSkills.find((x) => x.name === m.name))
|
|
241
|
+
activeSkills.push(m);
|
|
242
|
+
}
|
|
243
|
+
if (activeSkills.length > 0) {
|
|
244
|
+
setHistory((h) => [
|
|
245
|
+
...h,
|
|
246
|
+
mkItem('tool', 'skills active: ' + activeSkills.map((x) => x.name).join(', ')),
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
setPendingSkill(null);
|
|
250
|
+
for await (const ev of agent.run(trimmed, { activeSkills })) {
|
|
195
251
|
if (ev.type === 'text') {
|
|
196
252
|
textBuf += ev.delta;
|
|
197
253
|
setStreaming(textBuf);
|
|
@@ -255,6 +311,12 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
255
311
|
if (overlay === 'mcp') {
|
|
256
312
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(McpPanel, { onClose: () => setOverlay('none') }) }));
|
|
257
313
|
}
|
|
314
|
+
if (overlay === 'skillstore') {
|
|
315
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(SkillStorePanel, { onClose: () => setOverlay('none') }) }));
|
|
316
|
+
}
|
|
317
|
+
if (overlay === 'skills') {
|
|
318
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(SkillsPanel, { onClose: () => setOverlay('none') }) }));
|
|
319
|
+
}
|
|
258
320
|
if (overlay === 'settings') {
|
|
259
321
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(Settings, { onClose: () => setOverlay('none'), onModelChange: (m) => {
|
|
260
322
|
setCurrentModel(m);
|
package/dist/ui/Settings.js
CHANGED
|
@@ -72,6 +72,14 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
72
72
|
})(),
|
|
73
73
|
action: () => { setMcpCursor(0); setScreen('mcp'); },
|
|
74
74
|
},
|
|
75
|
+
{
|
|
76
|
+
label: 'Skills store URL',
|
|
77
|
+
hint: process.env.SKILLS_INDEX_URL || 'default (github · lumenlabsllc)',
|
|
78
|
+
action: () => {
|
|
79
|
+
setDraft(process.env.SKILLS_INDEX_URL || '');
|
|
80
|
+
setScreen('skillstore-url');
|
|
81
|
+
},
|
|
82
|
+
},
|
|
75
83
|
{ label: 'Close', hint: 'esc', action: onClose },
|
|
76
84
|
];
|
|
77
85
|
useInput((_, key) => {
|
|
@@ -276,6 +284,22 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
276
284
|
if (screen === 'mcp-confirm-del') {
|
|
277
285
|
return (_jsxs(Box, { flexDirection: "column", children: [top('delete server'), row(_jsxs(Text, { color: theme.warning, children: ["Delete MCP server \"", mcpConfirmDel, "\"?"] })), row(_jsx(Text, { color: theme.muted, children: "This removes it from mcp.json." })), bot, _jsxs(Text, { children: [_jsx(Text, { color: theme.success, bold: true, children: " [y]" }), _jsx(Text, { color: theme.muted, children: " yes " }), _jsx(Text, { color: theme.error, bold: true, children: "[n]" }), _jsx(Text, { color: theme.muted, children: " no" })] })] }));
|
|
278
286
|
}
|
|
287
|
+
if (screen === 'skillstore-url') {
|
|
288
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('skills store url'), row(_jsx(Text, { color: theme.muted, children: "Leave empty to use the default LUMEN store" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: draft, onChange: setDraft, placeholder: "https://raw.githubusercontent.com/you/repo/main/index.json", onSubmit: () => {
|
|
289
|
+
const v = draft.trim();
|
|
290
|
+
const next = { ...cfg };
|
|
291
|
+
if (v)
|
|
292
|
+
next.skillsIndexUrl = v;
|
|
293
|
+
else
|
|
294
|
+
delete next.skillsIndexUrl;
|
|
295
|
+
persist(next);
|
|
296
|
+
if (v)
|
|
297
|
+
process.env.SKILLS_INDEX_URL = v;
|
|
298
|
+
else
|
|
299
|
+
delete process.env.SKILLS_INDEX_URL;
|
|
300
|
+
setScreen('main');
|
|
301
|
+
} })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 save \u00B7 esc cancel" })] }));
|
|
302
|
+
}
|
|
279
303
|
if (screen === 'mcp-add-name') {
|
|
280
304
|
return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 1/4'), row(_jsx(Text, { color: theme.muted, children: "Name (unique, e.g. \"filesystem\")" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftName, onChange: setMcpDraftName, placeholder: "filesystem", onSubmit: () => setScreen('mcp-add-command') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
|
|
281
305
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { fetchStore, installSkill, uninstallSkill, updateAll, isInstalled, indexUrl, } from '../core/skillstore.js';
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
export const SkillStorePanel = ({ onClose, onChanged }) => {
|
|
7
|
+
const { stdout } = useStdout();
|
|
8
|
+
const width = Math.max(40, stdout?.columns ?? 60);
|
|
9
|
+
const inner = width - 4;
|
|
10
|
+
const [skills, setSkills] = useState([]);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const [idx, setIdx] = useState(0);
|
|
14
|
+
const [filter, setFilter] = useState('');
|
|
15
|
+
const [status, setStatus] = useState(null);
|
|
16
|
+
const [version, setVersion] = useState(0);
|
|
17
|
+
const reload = async (force = false) => {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
setError(null);
|
|
20
|
+
try {
|
|
21
|
+
const list = await fetchStore({ force });
|
|
22
|
+
setSkills(list);
|
|
23
|
+
setIdx(0);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
setError(err.message);
|
|
27
|
+
}
|
|
28
|
+
setLoading(false);
|
|
29
|
+
};
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
void reload(false);
|
|
32
|
+
}, []);
|
|
33
|
+
const filtered = useMemo(() => {
|
|
34
|
+
const q = filter.toLowerCase();
|
|
35
|
+
if (!q)
|
|
36
|
+
return skills;
|
|
37
|
+
return skills.filter((s) => s.name.toLowerCase().includes(q) ||
|
|
38
|
+
s.description.toLowerCase().includes(q) ||
|
|
39
|
+
(s.tags || []).some((t) => t.toLowerCase().includes(q)));
|
|
40
|
+
}, [skills, filter]);
|
|
41
|
+
const current = filtered[idx];
|
|
42
|
+
const flash = (msg) => {
|
|
43
|
+
setStatus(msg);
|
|
44
|
+
setTimeout(() => setStatus(null), 2500);
|
|
45
|
+
};
|
|
46
|
+
const doInstall = async () => {
|
|
47
|
+
if (!current)
|
|
48
|
+
return;
|
|
49
|
+
try {
|
|
50
|
+
await installSkill(current);
|
|
51
|
+
flash('✓ installed ' + current.name);
|
|
52
|
+
setVersion((v) => v + 1);
|
|
53
|
+
onChanged?.();
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
flash('✗ ' + err.message);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const doUninstall = () => {
|
|
60
|
+
if (!current)
|
|
61
|
+
return;
|
|
62
|
+
if (!isInstalled(current.name))
|
|
63
|
+
return;
|
|
64
|
+
try {
|
|
65
|
+
uninstallSkill(current.name);
|
|
66
|
+
flash('✓ removed ' + current.name);
|
|
67
|
+
setVersion((v) => v + 1);
|
|
68
|
+
onChanged?.();
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
flash('✗ ' + err.message);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const doUpdateAll = async () => {
|
|
75
|
+
setStatus('updating…');
|
|
76
|
+
try {
|
|
77
|
+
const res = await updateAll();
|
|
78
|
+
const updated = res.filter((r) => r.from !== r.to).length;
|
|
79
|
+
const failed = res.filter((r) => !r.ok).length;
|
|
80
|
+
flash(failed > 0
|
|
81
|
+
? `updated ${updated} · ${failed} failed`
|
|
82
|
+
: `✓ updated ${updated} skill${updated === 1 ? '' : 's'}`);
|
|
83
|
+
setVersion((v) => v + 1);
|
|
84
|
+
onChanged?.();
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
flash('✗ ' + err.message);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
useInput((input, key) => {
|
|
91
|
+
if (key.escape)
|
|
92
|
+
return onClose();
|
|
93
|
+
if (key.upArrow && filtered.length)
|
|
94
|
+
setIdx((i) => (i - 1 + filtered.length) % filtered.length);
|
|
95
|
+
if (key.downArrow && filtered.length)
|
|
96
|
+
setIdx((i) => (i + 1) % filtered.length);
|
|
97
|
+
if (input === 'i' || input === 'I') {
|
|
98
|
+
void doInstall();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (input === 'd' || input === 'D') {
|
|
102
|
+
doUninstall();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (input === 'u' || input === 'U') {
|
|
106
|
+
void doUpdateAll();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (input === 'r' || input === 'R') {
|
|
110
|
+
void reload(true);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (key.backspace || key.delete) {
|
|
114
|
+
setFilter((f) => f.slice(0, -1));
|
|
115
|
+
setIdx(0);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (input && !key.ctrl && !key.meta) {
|
|
119
|
+
const clean = input.replace(/[\x00-\x1F\x7F]/g, '');
|
|
120
|
+
if (clean) {
|
|
121
|
+
setFilter((f) => f + clean);
|
|
122
|
+
setIdx(0);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const label = filter ? `store · filter: ${filter}` : 'skill store';
|
|
127
|
+
const topPad = Math.max(1, inner - label.length - 4);
|
|
128
|
+
const maxVisible = 12;
|
|
129
|
+
const start = Math.max(0, Math.min(idx - Math.floor(maxVisible / 2), Math.max(0, filtered.length - maxVisible)));
|
|
130
|
+
const visible = filtered.slice(start, start + maxVisible);
|
|
131
|
+
const installedCount = skills.filter((s) => isInstalled(s.name)).length;
|
|
132
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: theme.accent, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(topPad) + '╮' })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: [skills.length, " skill", skills.length === 1 ? '' : 's', " \u00B7 ", installedCount, " installed"] }), status && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: status.startsWith('✗') ? theme.error : theme.success, children: status })] }))] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: indexUrl().slice(0, inner - 4) })] }), _jsx(Text, { color: theme.muted, children: '│ ' + '─'.repeat(Math.max(1, inner - 2)) }), loading ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: "fetching index\u2026" })] })) : error ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.error, children: ["\u2717 ", error.slice(0, inner - 6)] })] })) : filtered.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.warning, children: "no skills match" })] })) : (visible.map((s, i) => {
|
|
133
|
+
const realIdx = start + i;
|
|
134
|
+
const active = realIdx === idx;
|
|
135
|
+
const installed = isInstalled(s.name);
|
|
136
|
+
const marker = installed ? '✓ ' : ' ';
|
|
137
|
+
const markerColor = installed ? theme.success : theme.muted;
|
|
138
|
+
const tags = (s.tags || []).slice(0, 3).join(', ');
|
|
139
|
+
const right = installed ? 'installed' : (s.version ? 'v' + s.version : '');
|
|
140
|
+
const nameCap = Math.max(10, inner - right.length - 8);
|
|
141
|
+
const name = s.name.length > nameCap ? s.name.slice(0, nameCap - 1) + '…' : s.name;
|
|
142
|
+
const pad = Math.max(1, inner - name.length - right.length - 5);
|
|
143
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: markerColor, children: marker }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: name }), _jsx(Text, { color: theme.muted, children: ' '.repeat(pad) }), _jsx(Text, { color: theme.muted, children: right })] }), active && s.description && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: s.description.slice(0, inner - 8) })] })), active && tags && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ tags: ' }), _jsx(Text, { color: theme.muted, children: tags })] }))] }, s.name));
|
|
144
|
+
})), _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: " [" }), _jsx(Text, { color: theme.accent, children: "i" }), _jsx(Text, { color: theme.muted, children: "] install \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "d" }), _jsx(Text, { color: theme.muted, children: "] remove \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "u" }), _jsx(Text, { color: theme.muted, children: "] update all \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "r" }), _jsx(Text, { color: theme.muted, children: "] refresh \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "esc" }), _jsx(Text, { color: theme.muted, children: "] close" })] })] }));
|
|
145
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { listSkills, skillsDir } from '../core/skills.js';
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
export const SkillsPanel = ({ onClose }) => {
|
|
7
|
+
const { stdout } = useStdout();
|
|
8
|
+
const width = Math.max(40, stdout?.columns ?? 60);
|
|
9
|
+
const inner = width - 4;
|
|
10
|
+
const [skills, setSkills] = useState(() => listSkills());
|
|
11
|
+
useInput((input, key) => {
|
|
12
|
+
if (key.escape)
|
|
13
|
+
return onClose();
|
|
14
|
+
if (input === 'r' || input === 'R')
|
|
15
|
+
setSkills(listSkills());
|
|
16
|
+
});
|
|
17
|
+
const label = 'skills';
|
|
18
|
+
const topPad = Math.max(1, inner - label.length - 4);
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: theme.accent, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(topPad) + '╮' })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: ["dir: ", skillsDir()] })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: [skills.length, " skill", skills.length === 1 ? '' : 's', " loaded"] })] }), _jsx(Text, { color: theme.muted, children: '│ ' + '─'.repeat(Math.max(1, inner - 2)) }), skills.length === 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.warning, children: "no skills found" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: ["create ", skillsDir(), "/<name>/SKILL.md"] })] })] })) : (skills.map((s) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.success, children: '● ' }), _jsx(Text, { color: theme.text, bold: true, children: s.name })] }), s.description && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: s.description.slice(0, inner - 6) })] })), s.triggers.length > 0 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ triggers: ' }), _jsx(Text, { color: theme.muted, children: s.triggers.slice(0, 6).join(', ') })] }))] }, s.name)))), _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: " [" }), _jsx(Text, { color: theme.accent, children: "r" }), _jsx(Text, { color: theme.muted, children: "] reload \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "esc" }), _jsx(Text, { color: theme.muted, children: "] close \u00B7 use " }), _jsx(Text, { color: theme.accent, children: "/skill <name>" }), _jsx(Text, { color: theme.muted, children: " to force-activate" })] })] }));
|
|
20
|
+
};
|