teapot-coding-agent 0.22.13 → 0.24.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/README.md +29 -0
- package/dist/acp/adapter.js +198 -0
- package/dist/agent/agent.js +282 -15
- package/dist/agent/llm.js +33 -0
- package/dist/agent/skills.js +212 -32
- package/dist/agent/tools.js +776 -39
- package/dist/index.js +74 -27
- package/dist/log/events.js +14 -0
- package/dist/master.js +146 -14
- package/dist/model-meta.js +96 -36
- package/dist/server/api.js +44 -8
- package/dist/server/term-size.js +62 -0
- package/package.json +1 -1
- package/public/assets/index-B_XaPTQR.css +1 -0
- package/public/assets/index-Y_KYu5nW.js +30 -0
- package/public/assets/index-Y_KYu5nW.js.map +1 -0
- package/public/assets/{shiki-B-DhHuic.js → shiki-CG8vVdKn.js} +2 -2
- package/public/assets/{shiki-B-DhHuic.js.map → shiki-CG8vVdKn.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-B-RJz1_I.css +0 -1
- package/public/assets/index-Bg8Y4MGZ.js +0 -30
- package/public/assets/index-Bg8Y4MGZ.js.map +0 -1
package/dist/agent/skills.js
CHANGED
|
@@ -19,56 +19,227 @@
|
|
|
19
19
|
import { promises as fs } from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
export const SKILL_FILE = "SKILL.md";
|
|
22
|
+
/**
|
|
23
|
+
* Cheap change-detector for a set of skill roots. Returns a stable string
|
|
24
|
+
* that changes whenever a root is added, removed, or modified — so callers
|
|
25
|
+
* can skip a full rescan (a readdir per root plus a read of every SKILL.md)
|
|
26
|
+
* on the overwhelmingly common "nothing changed" path.
|
|
27
|
+
*
|
|
28
|
+
* The fingerprint covers each root's own mtime/size AND the mtime/size of
|
|
29
|
+
* every SKILL.md one level down, because writing SKILL.md does not bump the
|
|
30
|
+
* parent directory's mtime. Helper scripts edited in place are covered by
|
|
31
|
+
* the SKILL.md stat only in aggregate; a rewritten SKILL.md always lands
|
|
32
|
+
* here too (saveSkill rewrites it), so bundled-file edits ride along with
|
|
33
|
+
* the next skill write rather than triggering their own rescan.
|
|
34
|
+
*/
|
|
35
|
+
export async function skillRootsFingerprint(roots) {
|
|
36
|
+
const parts = [];
|
|
37
|
+
for (const root of roots) {
|
|
38
|
+
parts.push(root.dir);
|
|
39
|
+
let st = null;
|
|
40
|
+
try {
|
|
41
|
+
st = await fs.stat(root.dir);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
parts.push("-"); // root missing → no skills here (same as discoverSkills)
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
parts.push(`${st.mtimeMs}:${st.size}`);
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = await fs.readdir(root.dir, { withFileTypes: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const e of entries) {
|
|
56
|
+
if (!e.isDirectory() || e.name.startsWith("."))
|
|
57
|
+
continue;
|
|
58
|
+
try {
|
|
59
|
+
const s = await fs.stat(path.join(root.dir, e.name, SKILL_FILE));
|
|
60
|
+
parts.push(`${e.name}=${s.mtimeMs}:${s.size}`);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
parts.push(`${e.name}=none`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return parts.join("|");
|
|
68
|
+
}
|
|
22
69
|
export function isValidSkillName(name) {
|
|
23
70
|
return /^[a-z0-9][a-z0-9._-]{0,63}$/.test(name);
|
|
24
71
|
}
|
|
25
72
|
/**
|
|
26
73
|
* Minimal frontmatter parser for the subset we need: a leading `---` block of
|
|
27
74
|
* `key: value` lines. No YAML dependency by design.
|
|
75
|
+
*
|
|
76
|
+
* It does understand the two YAML shapes that are common in REAL third-party
|
|
77
|
+
* skills (#28), because without them those files are unreadable:
|
|
78
|
+
* - FOLDED/LITERAL BLOCK SCALARS (`description: >-` … or `|` …), which is how
|
|
79
|
+
* most skill authors write a long description. Reading them naively yields
|
|
80
|
+
* the literal ">-" as the description, which silently destroys the one field
|
|
81
|
+
* the model matches its task against.
|
|
82
|
+
* - QUOTED values, whose surrounding quotes are stripped so a description
|
|
83
|
+
* containing a colon survives.
|
|
84
|
+
* A key whose value is empty (e.g. a nested `metadata:` block) is left out rather
|
|
85
|
+
* than recorded as an empty string.
|
|
28
86
|
*/
|
|
29
87
|
export function parseSkillMd(text) {
|
|
30
88
|
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
31
89
|
if (!m)
|
|
32
90
|
return { meta: {}, body: text.trim() };
|
|
91
|
+
const lines = m[1].split(/\r?\n/);
|
|
33
92
|
const meta = {};
|
|
34
|
-
for (
|
|
35
|
-
const kv =
|
|
36
|
-
if (kv)
|
|
37
|
-
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const kv = lines[i].match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
95
|
+
if (!kv)
|
|
96
|
+
continue;
|
|
97
|
+
const key = kv[1].toLowerCase();
|
|
98
|
+
const raw = kv[2].trim();
|
|
99
|
+
// folded (>-, >, |-, |) or literal (|, |-) block scalar: the value is the
|
|
100
|
+
// following, more-indented lines
|
|
101
|
+
if (/^[>|][-+]?$/.test(raw)) {
|
|
102
|
+
const parts = [];
|
|
103
|
+
let j = i + 1;
|
|
104
|
+
for (; j < lines.length; j++) {
|
|
105
|
+
const l = lines[j];
|
|
106
|
+
if (l.trim() === "") {
|
|
107
|
+
parts.push("");
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!/^\s/.test(l))
|
|
111
|
+
break; // dedented → block ended
|
|
112
|
+
parts.push(l.trim());
|
|
113
|
+
}
|
|
114
|
+
i = j - 1;
|
|
115
|
+
const joined = parts.join(/^[>|][-+]?$/.test(raw) && raw.startsWith("|") ? "\n" : " ").trim();
|
|
116
|
+
if (joined)
|
|
117
|
+
meta[key] = joined;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (raw === "")
|
|
121
|
+
continue; // `metadata:` with a nested block below
|
|
122
|
+
meta[key] = unquote(raw);
|
|
38
123
|
}
|
|
39
124
|
return { meta, body: m[2].trim() };
|
|
40
125
|
}
|
|
126
|
+
/** Strip one layer of matching quotes; a quoted value may itself contain `:`. */
|
|
127
|
+
function unquote(v) {
|
|
128
|
+
const s = v.trim();
|
|
129
|
+
if (s.length >= 2 && ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'")))) {
|
|
130
|
+
return s
|
|
131
|
+
.slice(1, -1)
|
|
132
|
+
.replace(/\\"/g, '"')
|
|
133
|
+
.replace(/''/g, "'");
|
|
134
|
+
}
|
|
135
|
+
return s;
|
|
136
|
+
}
|
|
137
|
+
/** Root dirs in priority order: [0] overrides later ones on name clash. */
|
|
138
|
+
/**
|
|
139
|
+
* Skill directories owned by OTHER coding harnesses (#28).
|
|
140
|
+
*
|
|
141
|
+
* SKILL.md is a shared convention, so these are readable as-is — the only
|
|
142
|
+
* thing that ever blocked them was our frontmatter parser (folded scalars and
|
|
143
|
+
* quoted values), now handled. Paths are the documented native locations,
|
|
144
|
+
* plus the cross-tool directories several tools also read.
|
|
145
|
+
*
|
|
146
|
+
* Priority is deliberately the LOWEST of the user-supplied roots: a project
|
|
147
|
+
* should never be shadowed by another tool's copy of the same skill. Missing
|
|
148
|
+
* directories are skipped silently, and nothing is read unless it exists.
|
|
149
|
+
*/
|
|
150
|
+
export function foreignSkillRoots(workspace) {
|
|
151
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
152
|
+
const out = [];
|
|
153
|
+
const add = (dir) => {
|
|
154
|
+
if (dir && !out.some((r) => r.dir === dir))
|
|
155
|
+
out.push({ dir, source: "foreign" });
|
|
156
|
+
};
|
|
157
|
+
if (workspace) {
|
|
158
|
+
// project-local locations other tools read
|
|
159
|
+
for (const rel of [
|
|
160
|
+
path.join(".agents", "skills"), // cross-tool convention
|
|
161
|
+
path.join(".claude", "skills"),
|
|
162
|
+
path.join(".codex", "skills"),
|
|
163
|
+
path.join(".cursor", "skills"),
|
|
164
|
+
path.join(".gemini", "skills"),
|
|
165
|
+
path.join(".github", "skills"), // Copilot CLI + VS Code
|
|
166
|
+
path.join(".opencode", "skills"),
|
|
167
|
+
]) {
|
|
168
|
+
add(path.join(workspace, rel));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (home) {
|
|
172
|
+
add(path.join(home, ".agents", "skills")); // cross-tool, personal
|
|
173
|
+
for (const rel of [
|
|
174
|
+
[".claude", "skills"],
|
|
175
|
+
[".codex", "skills"],
|
|
176
|
+
[".cursor", "skills"],
|
|
177
|
+
[".gemini", "skills"],
|
|
178
|
+
[".copilot", "skills"], // GitHub Copilot CLI + VS Code share this
|
|
179
|
+
[".config", "opencode", "skills"],
|
|
180
|
+
]) {
|
|
181
|
+
add(path.join(home, ...rel));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* How deep to look for nested skills (#27).
|
|
188
|
+
*
|
|
189
|
+
* Bounded because a skill directory also holds BUNDLED FILES (helper scripts)
|
|
190
|
+
* and arbitrary payload, so an unbounded walk is not safe. Depth is not the
|
|
191
|
+
* thing that makes a directory a skill, though — holding a SKILL.md is — so the
|
|
192
|
+
* bound only limits how far we look, never what counts.
|
|
193
|
+
*/
|
|
194
|
+
const MAX_SKILL_DEPTH = 4;
|
|
41
195
|
/** Root dirs in priority order: [0] overrides later ones on name clash. */
|
|
42
196
|
export async function discoverSkills(roots) {
|
|
43
197
|
const byName = new Map();
|
|
198
|
+
// name -> depth, so a SHALLOWER skill wins over a nested namesake rather than
|
|
199
|
+
// a deep one silently shadowing the obvious skill (#27)
|
|
200
|
+
const depthByName = new Map();
|
|
44
201
|
for (const root of roots) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
202
|
+
await walkSkills(root.dir, root.source, 0, byName, depthByName);
|
|
203
|
+
}
|
|
204
|
+
return [...byName.values()];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Depth-first walk collecting every directory that holds a SKILL.md (#27).
|
|
208
|
+
*
|
|
209
|
+
* A directory WITHOUT its own SKILL.md is only a container to descend into — we
|
|
210
|
+
* never treat one as a skill, which is what keeps bundled helper directories
|
|
211
|
+
* from being reported as skills themselves.
|
|
212
|
+
*/
|
|
213
|
+
async function walkSkills(dir, source, depth, byName, depthByName) {
|
|
214
|
+
if (depth > MAX_SKILL_DEPTH)
|
|
215
|
+
return;
|
|
216
|
+
let entries;
|
|
217
|
+
try {
|
|
218
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return; // root missing → simply no skills there
|
|
222
|
+
}
|
|
223
|
+
// a SKILL.md sitting directly in `dir` makes `dir` a skill AND a container
|
|
224
|
+
const here = path.join(dir, SKILL_FILE);
|
|
225
|
+
let hereText = null;
|
|
226
|
+
try {
|
|
227
|
+
hereText = await fs.readFile(here, "utf8");
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
hereText = null;
|
|
231
|
+
}
|
|
232
|
+
if (hereText !== null && depth > 0) {
|
|
233
|
+
const parsed = parseSkillMd(hereText);
|
|
234
|
+
const name = parsed.meta.name || path.basename(dir);
|
|
235
|
+
const prevDepth = depthByName.get(name);
|
|
236
|
+
// first root wins on a name clash, and a shallower skill beats a deeper one
|
|
237
|
+
const wins = prevDepth === undefined ||
|
|
238
|
+
(prevDepth !== undefined && depth < prevDepth && byName.get(name)?.source === source);
|
|
239
|
+
if (wins) {
|
|
68
240
|
let files = [];
|
|
69
241
|
try {
|
|
70
|
-
|
|
71
|
-
files = siblings
|
|
242
|
+
files = (await fs.readdir(dir, { withFileTypes: true }))
|
|
72
243
|
.filter((f) => f.isFile() && f.name !== SKILL_FILE && !f.name.startsWith("."))
|
|
73
244
|
.map((f) => f.name)
|
|
74
245
|
.sort();
|
|
@@ -79,13 +250,22 @@ export async function discoverSkills(roots) {
|
|
|
79
250
|
byName.set(name, {
|
|
80
251
|
name,
|
|
81
252
|
description: parsed.meta.description || "",
|
|
82
|
-
source
|
|
83
|
-
filePath,
|
|
253
|
+
source,
|
|
254
|
+
filePath: here,
|
|
84
255
|
files,
|
|
85
256
|
});
|
|
257
|
+
depthByName.set(name, depth);
|
|
86
258
|
}
|
|
87
259
|
}
|
|
88
|
-
|
|
260
|
+
for (const e of entries) {
|
|
261
|
+
if (!e.isDirectory() || e.name.startsWith("."))
|
|
262
|
+
continue;
|
|
263
|
+
// Recurse into every subdirectory: nesting means a skill can sit at ANY depth
|
|
264
|
+
// (skills/stage1/template/SKILL.md), and "stage1" itself has no SKILL.md.
|
|
265
|
+
// Bundled helper payloads are not mistaken for skills because a directory
|
|
266
|
+
// only BECOMES a skill when it holds its own SKILL.md (#27).
|
|
267
|
+
await walkSkills(path.join(dir, e.name), source, depth + 1, byName, depthByName);
|
|
268
|
+
}
|
|
89
269
|
}
|
|
90
270
|
export async function readSkillFile(def) {
|
|
91
271
|
return fs.readFile(def.filePath, "utf8");
|