teapot-coding-agent 0.23.0 → 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/dist/acp/adapter.js +198 -0
- package/dist/agent/agent.js +263 -12
- 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 +14 -0
- package/dist/master.js +134 -12
- package/dist/model-meta.js +96 -36
- package/dist/server/api.js +21 -5
- 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-NOpdigkY.js → shiki-CG8vVdKn.js} +2 -2
- package/public/assets/{shiki-NOpdigkY.js.map → shiki-CG8vVdKn.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-CNmd6GUS.js +0 -30
- package/public/assets/index-CNmd6GUS.js.map +0 -1
- package/public/assets/index-QCwmIYsM.css +0 -1
package/dist/agent/llm.js
CHANGED
|
@@ -7,6 +7,37 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import OpenAI from "openai";
|
|
9
9
|
const clients = new WeakMap();
|
|
10
|
+
/**
|
|
11
|
+
* Reasoning effort for a request body (#47).
|
|
12
|
+
*
|
|
13
|
+
* Kept local and deliberately conservative: the setting is only honoured when
|
|
14
|
+
* the endpoint is OpenRouter, the value is one OpenRouter documents, and the
|
|
15
|
+
* catalogue says this model advertises `reasoning_effort`. Support is far from
|
|
16
|
+
* universal (verified against the live /models endpoint: 196 of 462 models),
|
|
17
|
+
* so an unsent field is much safer than a wrong one.
|
|
18
|
+
*
|
|
19
|
+
* Implemented here rather than via model-meta to avoid an import cycle —
|
|
20
|
+
* model-meta already imports providerHeaders from this file.
|
|
21
|
+
*/
|
|
22
|
+
export function effortForRequest(cfg, supported = cfg.supportedParameters) {
|
|
23
|
+
const e = cfg.reasoningEffort;
|
|
24
|
+
if (!e || !REASONING_EFFORTS.includes(e))
|
|
25
|
+
return {};
|
|
26
|
+
if (!isOpenRouterEndpoint(cfg.baseUrl))
|
|
27
|
+
return {};
|
|
28
|
+
if (!supported?.includes("reasoning_effort"))
|
|
29
|
+
return {};
|
|
30
|
+
return { reasoning_effort: e };
|
|
31
|
+
}
|
|
32
|
+
function isOpenRouterEndpoint(baseUrl) {
|
|
33
|
+
try {
|
|
34
|
+
return /(^|\.)openrouter\.ai$/i.test(new URL(String(baseUrl ?? "")).host);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
|
|
10
41
|
/**
|
|
11
42
|
* OpenRouter app attribution (https://openrouter.ai/docs/app-attribution):
|
|
12
43
|
* `HTTP-Referer` is the required identifier that puts teapot's usage on the
|
|
@@ -103,6 +134,7 @@ export async function chat(cfg, messages, tools, signal, onDelta) {
|
|
|
103
134
|
model: cfg.model,
|
|
104
135
|
messages: sanitize(messages),
|
|
105
136
|
...(tools.length ? { tools } : {}),
|
|
137
|
+
...effortForRequest(cfg),
|
|
106
138
|
}, { signal });
|
|
107
139
|
const raw = res.choices?.[0];
|
|
108
140
|
const rm = raw?.message;
|
|
@@ -188,6 +220,7 @@ export async function chatStream(cfg, messages, tools, signal, onDelta) {
|
|
|
188
220
|
model: cfg.model,
|
|
189
221
|
messages: sanitize(messages),
|
|
190
222
|
...(tools.length ? { tools } : {}),
|
|
223
|
+
...effortForRequest(cfg),
|
|
191
224
|
stream: true,
|
|
192
225
|
// ask providers to include the final usage chunk in streaming mode
|
|
193
226
|
// (OpenAI-compatible; harmless where unsupported)
|
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");
|