teapot-coding-agent 0.23.0 → 0.24.1
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/acp/adapter.js +198 -0
- package/dist/agent/agent.js +305 -20
- package/dist/agent/llm.js +33 -0
- package/dist/agent/skills.js +165 -32
- package/dist/agent/tools.js +632 -27
- package/dist/index.js +74 -27
- package/dist/log/events.js +96 -0
- package/dist/master.js +134 -12
- package/dist/model-meta.js +96 -36
- package/dist/server/api.js +42 -9
- package/package.json +1 -1
- package/public/assets/index-ClyCarLt.css +1 -0
- package/public/assets/{index-CNmd6GUS.js → index-sfdzqjpM.js} +20 -20
- package/public/assets/index-sfdzqjpM.js.map +1 -0
- package/public/assets/{shiki-NOpdigkY.js → shiki-D1TWrY4w.js} +2 -2
- package/public/assets/{shiki-NOpdigkY.js.map → shiki-D1TWrY4w.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-CNmd6GUS.js.map +0 -1
- package/public/assets/index-QCwmIYsM.css +0 -1
package/dist/agent/skills.js
CHANGED
|
@@ -72,50 +72,174 @@ export function isValidSkillName(name) {
|
|
|
72
72
|
/**
|
|
73
73
|
* Minimal frontmatter parser for the subset we need: a leading `---` block of
|
|
74
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.
|
|
75
86
|
*/
|
|
76
87
|
export function parseSkillMd(text) {
|
|
77
88
|
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
78
89
|
if (!m)
|
|
79
90
|
return { meta: {}, body: text.trim() };
|
|
91
|
+
const lines = m[1].split(/\r?\n/);
|
|
80
92
|
const meta = {};
|
|
81
|
-
for (
|
|
82
|
-
const kv =
|
|
83
|
-
if (kv)
|
|
84
|
-
|
|
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);
|
|
85
123
|
}
|
|
86
124
|
return { meta, body: m[2].trim() };
|
|
87
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;
|
|
88
195
|
/** Root dirs in priority order: [0] overrides later ones on name clash. */
|
|
89
196
|
export async function discoverSkills(roots) {
|
|
90
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();
|
|
91
201
|
for (const root of roots) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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) {
|
|
115
240
|
let files = [];
|
|
116
241
|
try {
|
|
117
|
-
|
|
118
|
-
files = siblings
|
|
242
|
+
files = (await fs.readdir(dir, { withFileTypes: true }))
|
|
119
243
|
.filter((f) => f.isFile() && f.name !== SKILL_FILE && !f.name.startsWith("."))
|
|
120
244
|
.map((f) => f.name)
|
|
121
245
|
.sort();
|
|
@@ -126,13 +250,22 @@ export async function discoverSkills(roots) {
|
|
|
126
250
|
byName.set(name, {
|
|
127
251
|
name,
|
|
128
252
|
description: parsed.meta.description || "",
|
|
129
|
-
source
|
|
130
|
-
filePath,
|
|
253
|
+
source,
|
|
254
|
+
filePath: here,
|
|
131
255
|
files,
|
|
132
256
|
});
|
|
257
|
+
depthByName.set(name, depth);
|
|
133
258
|
}
|
|
134
259
|
}
|
|
135
|
-
|
|
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
|
+
}
|
|
136
269
|
}
|
|
137
270
|
export async function readSkillFile(def) {
|
|
138
271
|
return fs.readFile(def.filePath, "utf8");
|