blyuecode 0.3.8 → 0.4.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/skills.js +88 -0
- package/dist/ui/Chat.js +53 -1
- 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();
|
|
@@ -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
|
+
}
|
package/dist/ui/Chat.js
CHANGED
|
@@ -19,6 +19,8 @@ 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 { listSkills, findSkill, matchSkills, ensureSkillsDir, } from '../core/skills.js';
|
|
22
24
|
let nextId = 0;
|
|
23
25
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
24
26
|
const HistoryLine = ({ item }) => {
|
|
@@ -67,6 +69,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
67
69
|
const [loading, setLoading] = useState(false);
|
|
68
70
|
const [streaming, setStreaming] = useState('');
|
|
69
71
|
const [overlay, setOverlay] = useState('none');
|
|
72
|
+
const [pendingSkill, setPendingSkill] = useState(null);
|
|
70
73
|
const [permReq, setPermReq] = useState(null);
|
|
71
74
|
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
72
75
|
const [agent, setAgent] = useState(() => new Agent());
|
|
@@ -105,6 +108,10 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
105
108
|
if (key.ctrl && inputChar === 'o')
|
|
106
109
|
setOverlay('settings');
|
|
107
110
|
});
|
|
111
|
+
useEffect(() => { try {
|
|
112
|
+
ensureSkillsDir();
|
|
113
|
+
}
|
|
114
|
+
catch { } }, []);
|
|
108
115
|
const flush = (kind, text) => {
|
|
109
116
|
if (!text)
|
|
110
117
|
return;
|
|
@@ -130,6 +137,31 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
130
137
|
setOverlay('mcp');
|
|
131
138
|
return;
|
|
132
139
|
}
|
|
140
|
+
if (trimmed === '/skills') {
|
|
141
|
+
setInput('');
|
|
142
|
+
setOverlay('skills');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (trimmed.startsWith('/skill ')) {
|
|
146
|
+
setInput('');
|
|
147
|
+
const name = trimmed.slice(7).trim();
|
|
148
|
+
const sk = findSkill(name);
|
|
149
|
+
if (!sk) {
|
|
150
|
+
const all = listSkills().map((x) => x.name).join(', ') || '(none)';
|
|
151
|
+
setHistory((h) => [
|
|
152
|
+
...h,
|
|
153
|
+
mkItem('error', 'unknown skill: ' + name),
|
|
154
|
+
mkItem('tool', 'available: ' + all),
|
|
155
|
+
]);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
setPendingSkill(sk);
|
|
159
|
+
setHistory((h) => [
|
|
160
|
+
...h,
|
|
161
|
+
mkItem('tool', 'skill armed: ' + sk.name + ' (applies to next message)'),
|
|
162
|
+
]);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
133
165
|
if (trimmed === '/debug') {
|
|
134
166
|
setInput('');
|
|
135
167
|
const lines = readDebug();
|
|
@@ -176,6 +208,8 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
176
208
|
mkItem('tool', ' /model switch model (Ctrl+L)'),
|
|
177
209
|
mkItem('tool', ' /settings open settings (Ctrl+O)'),
|
|
178
210
|
mkItem('tool', ' /mcp show MCP servers and tools'),
|
|
211
|
+
mkItem('tool', ' /skills list skills'),
|
|
212
|
+
mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
|
|
179
213
|
mkItem('tool', ' /compact summarize history to free context'),
|
|
180
214
|
mkItem('tool', ' /debug show last request to provider'),
|
|
181
215
|
mkItem('tool', ' /clear clear session and permissions'),
|
|
@@ -191,7 +225,22 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
191
225
|
let textBuf = '';
|
|
192
226
|
let toolBuf = '';
|
|
193
227
|
try {
|
|
194
|
-
|
|
228
|
+
const matched = matchSkills(trimmed);
|
|
229
|
+
const activeSkills = [];
|
|
230
|
+
if (pendingSkill)
|
|
231
|
+
activeSkills.push(pendingSkill);
|
|
232
|
+
for (const m of matched) {
|
|
233
|
+
if (!activeSkills.find((x) => x.name === m.name))
|
|
234
|
+
activeSkills.push(m);
|
|
235
|
+
}
|
|
236
|
+
if (activeSkills.length > 0) {
|
|
237
|
+
setHistory((h) => [
|
|
238
|
+
...h,
|
|
239
|
+
mkItem('tool', 'skills active: ' + activeSkills.map((x) => x.name).join(', ')),
|
|
240
|
+
]);
|
|
241
|
+
}
|
|
242
|
+
setPendingSkill(null);
|
|
243
|
+
for await (const ev of agent.run(trimmed, { activeSkills })) {
|
|
195
244
|
if (ev.type === 'text') {
|
|
196
245
|
textBuf += ev.delta;
|
|
197
246
|
setStreaming(textBuf);
|
|
@@ -255,6 +304,9 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
255
304
|
if (overlay === 'mcp') {
|
|
256
305
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(McpPanel, { onClose: () => setOverlay('none') }) }));
|
|
257
306
|
}
|
|
307
|
+
if (overlay === 'skills') {
|
|
308
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(SkillsPanel, { onClose: () => setOverlay('none') }) }));
|
|
309
|
+
}
|
|
258
310
|
if (overlay === 'settings') {
|
|
259
311
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(Settings, { onClose: () => setOverlay('none'), onModelChange: (m) => {
|
|
260
312
|
setCurrentModel(m);
|
|
@@ -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
|
+
};
|