agent-from-zero 0.35.1 → 0.35.3
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 +141 -0
- package/dist/agent.js +43 -26
- package/dist/completion.js +93 -3
- package/dist/config.js +38 -1
- package/dist/context.js +46 -6
- package/dist/ink/app.js +158 -30
- package/dist/ink/chat.js +3 -1
- package/dist/ink/commands.js +6 -3
- package/dist/ink/completion.js +13 -5
- package/dist/ink/setup.js +16 -15
- package/dist/ink/skills-panel.js +86 -0
- package/dist/loop.js +34 -8
- package/dist/mcp.js +332 -16
- package/dist/skills.js +282 -41
- package/dist/ui.js +8 -1
- package/package.json +2 -1
package/dist/skills.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs"; // discover and read SKILL.md files
|
|
2
2
|
import os from "node:os"; // the global skills dir
|
|
3
3
|
import path from "node:path"; // path resolution
|
|
4
|
+
import { CONFIG, saveGlobalSetting } from "./config.js"; // the listing budget + /skills on/off choices
|
|
5
|
+
import { registerExternalTool, tools, unregisterExternalTool } from "./tools.js"; // a skill is exposed to the model as a tool
|
|
4
6
|
// Project skills win over global ones with the same name.
|
|
5
7
|
function skillDirs() {
|
|
6
8
|
return [path.resolve(".mini-agent", "skills"), path.join(os.homedir(), ".config", "mini-agent", "skills")];
|
|
@@ -23,17 +25,22 @@ export function parseSkill(raw, fallbackName, filePath = "") {
|
|
|
23
25
|
fm[k] = v;
|
|
24
26
|
}
|
|
25
27
|
}
|
|
28
|
+
// No description? Claude Code falls back to the body's first line — something beats nothing in the listing.
|
|
29
|
+
const firstLine = body.split("\n").find((l) => l.trim())?.replace(/^#+\s*/, "").trim() ?? "";
|
|
26
30
|
return {
|
|
27
31
|
name: fm.name || fallbackName,
|
|
28
|
-
description: fm.description ||
|
|
29
|
-
whenToUse: fm.when_to_use || fm.whenToUse ||
|
|
32
|
+
description: fm.description || firstLine,
|
|
33
|
+
whenToUse: fm.when_to_use || fm.whenToUse || "",
|
|
30
34
|
allowedTools: (fm["allowed-tools"] || fm.allowedTools || "")
|
|
31
35
|
.split(",")
|
|
32
36
|
.map((s) => s.trim())
|
|
33
37
|
.filter(Boolean),
|
|
34
38
|
disableModelInvocation: /^true$/i.test(fm.disableModelInvocation || fm["disable-model-invocation"] || ""),
|
|
39
|
+
argumentHint: fm["argument-hint"] || "",
|
|
40
|
+
argNames: (fm.arguments || "").replace(/^\[|\]$/g, "").split(/[\s,]+/).map((a) => a.replace(/^["']|["']$/g, "")).filter(Boolean), // "a, b" or "[a, b]"
|
|
35
41
|
body,
|
|
36
42
|
path: filePath,
|
|
43
|
+
dir: filePath ? path.dirname(filePath) : "",
|
|
37
44
|
};
|
|
38
45
|
}
|
|
39
46
|
// Discover skills under the skill dirs (project first, then global). A bad file
|
|
@@ -65,64 +72,298 @@ export function loadSkills(dirs = skillDirs()) {
|
|
|
65
72
|
}
|
|
66
73
|
return out;
|
|
67
74
|
}
|
|
75
|
+
// ---- hot reload -------------------------------------------------------------------
|
|
76
|
+
// Claude Code runs a chokidar watcher over its skill dirs. We get the same
|
|
77
|
+
// effect with no watcher at all: before every model call (and whenever the UI
|
|
78
|
+
// asks), stat the SKILL.md files and compare a fingerprint — every path with its
|
|
79
|
+
// mtime and size. A few readdir + stat calls per round is nothing next to a
|
|
80
|
+
// model call, and there is no timer to leak, debounce, or race.
|
|
81
|
+
let cache;
|
|
82
|
+
function fingerprint(dirs) {
|
|
83
|
+
const parts = [];
|
|
84
|
+
for (const dir of dirs) {
|
|
85
|
+
let entries = [];
|
|
86
|
+
try {
|
|
87
|
+
entries = fs.readdirSync(dir);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries.sort()) {
|
|
93
|
+
try {
|
|
94
|
+
const st = fs.statSync(path.join(dir, entry, "SKILL.md"));
|
|
95
|
+
parts.push(`${dir}/${entry}:${st.mtimeMs}:${st.size}`);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* no SKILL.md here */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return parts.join("|");
|
|
103
|
+
}
|
|
104
|
+
// Every skill on disk, whatever its /skills setting: reloaded only when a
|
|
105
|
+
// SKILL.md was added, edited, or removed since the last call. `dirs` is
|
|
106
|
+
// injectable for tests. The /skills panel lists these.
|
|
107
|
+
export function allSkills(dirs = skillDirs()) {
|
|
108
|
+
const key = dirs.join("\n");
|
|
109
|
+
const fp = fingerprint(dirs);
|
|
110
|
+
if (!cache || cache.key !== key || cache.fingerprint !== fp)
|
|
111
|
+
cache = { key, fingerprint: fp, skills: loadSkills(dirs) };
|
|
112
|
+
return cache.skills;
|
|
113
|
+
}
|
|
114
|
+
// The skills in effect — what the listing, the skill tool and /skill see.
|
|
115
|
+
// The /skills choices apply here: "off" skills vanish entirely, "user-only"
|
|
116
|
+
// ones stay runnable by the user but are hidden from the model.
|
|
117
|
+
export function currentSkills(dirs = skillDirs()) {
|
|
118
|
+
return allSkills(dirs)
|
|
119
|
+
.filter((s) => skillMode(s) !== "off")
|
|
120
|
+
.map((s) => (skillMode(s) === "user-only" && !s.disableModelInvocation ? { ...s, disableModelInvocation: true } : s));
|
|
121
|
+
}
|
|
122
|
+
export const skillLocked = (s) => s.disableModelInvocation;
|
|
123
|
+
export function skillMode(s) {
|
|
124
|
+
if (skillLocked(s))
|
|
125
|
+
return "user-only";
|
|
126
|
+
return CONFIG.skillOverrides[s.name] ?? "on";
|
|
127
|
+
}
|
|
128
|
+
// Enter/space in the panel: on → user-only → off → on.
|
|
129
|
+
export const nextSkillMode = (m) => (m === "on" ? "user-only" : m === "user-only" ? "off" : "on");
|
|
130
|
+
// Apply and persist a choice. "on" is the default, so it removes the entry
|
|
131
|
+
// rather than storing it — the settings file only lists exceptions.
|
|
132
|
+
export function setSkillMode(name, mode, persist = true) {
|
|
133
|
+
const next = { ...CONFIG.skillOverrides };
|
|
134
|
+
if (mode === "on")
|
|
135
|
+
delete next[name];
|
|
136
|
+
else
|
|
137
|
+
next[name] = mode;
|
|
138
|
+
CONFIG.skillOverrides = next;
|
|
139
|
+
if (persist)
|
|
140
|
+
saveGlobalSetting("skillOverrides", next);
|
|
141
|
+
}
|
|
142
|
+
// One panel row's facts: where it lives and what it costs the model to know
|
|
143
|
+
// about it (its listing line, ~4 bytes per token — the same estimate as context.ts).
|
|
144
|
+
export function skillSource(s) {
|
|
145
|
+
return s.path.startsWith(path.resolve(".mini-agent", "skills")) ? "project" : "user";
|
|
146
|
+
}
|
|
147
|
+
export const skillListingTokens = (s) => Math.ceil(Buffer.byteLength(listingEntry(s)) / 4);
|
|
148
|
+
export const nextSkillSort = (s) => (s === "name" ? "tokens" : s === "tokens" ? "source" : "name");
|
|
149
|
+
// The panel's list: filtered by the search box, sorted.
|
|
150
|
+
// Matching is about the NAME: any part of it ("pul" finds co_dev_pulse). The
|
|
151
|
+
// description only counts for a real word — 3+ characters that start one of
|
|
152
|
+
// its words ("standup", "pulse") — and those rows come after the name matches.
|
|
153
|
+
// A plain substring over descriptions matched everything: a single "a" is in
|
|
154
|
+
// almost every sentence.
|
|
155
|
+
export function skillPanelRows(skills, query, sort) {
|
|
156
|
+
const q = query.trim().toLowerCase();
|
|
157
|
+
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
158
|
+
const order = (rows) => sort === "tokens" ? rows.sort((a, b) => skillListingTokens(b) - skillListingTokens(a) || byName(a, b))
|
|
159
|
+
: sort === "source" ? rows.sort((a, b) => skillSource(a).localeCompare(skillSource(b)) || byName(a, b))
|
|
160
|
+
: rows.sort(byName);
|
|
161
|
+
if (!q)
|
|
162
|
+
return order([...skills]);
|
|
163
|
+
const inName = skills.filter((s) => s.name.toLowerCase().includes(q));
|
|
164
|
+
const inWords = q.length >= 3 ? skills.filter((s) => !inName.includes(s) && s.description.toLowerCase().split(/[^a-z0-9]+/).some((w) => w.startsWith(q))) : [];
|
|
165
|
+
return [...order(inName), ...order(inWords)];
|
|
166
|
+
}
|
|
68
167
|
export function findSkill(skills, name) {
|
|
69
|
-
const n = name.trim().toLowerCase();
|
|
168
|
+
const n = name.trim().replace(/^\//, "").toLowerCase(); // "/deploy" and "deploy" both work, like Claude Code
|
|
70
169
|
return skills.find((s) => s.name.toLowerCase() === n);
|
|
71
170
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
used += line.length;
|
|
86
|
-
lines.push(line);
|
|
171
|
+
// ---- usage: what the "/" typeahead puts first -----------------------------------------
|
|
172
|
+
// Claude Code ranks a bare "/" by how much you use each skill: every run you
|
|
173
|
+
// start yourself (/name or /skill name) counts, and old use fades — the score
|
|
174
|
+
// halves every 7 days, never below a tenth. Kept in its own small file (not in
|
|
175
|
+
// settings.json, which is for choices, not counters).
|
|
176
|
+
// MINI_AGENT_SKILL_USAGE relocates it (tests point it at a scratch file).
|
|
177
|
+
const usagePath = () => process.env.MINI_AGENT_SKILL_USAGE || path.join(os.homedir(), ".config", "mini-agent", "skill-usage.json");
|
|
178
|
+
function readUsage() {
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(fs.readFileSync(usagePath(), "utf8"));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return {}; // none yet, or unreadable — ranking just falls back to groups + names
|
|
87
184
|
}
|
|
88
|
-
return lines.join("\n");
|
|
89
185
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
186
|
+
export function recordSkillUsage(name, now = Date.now()) {
|
|
187
|
+
const usage = readUsage();
|
|
188
|
+
usage[name] = { usageCount: (usage[name]?.usageCount ?? 0) + 1, lastUsedAt: now };
|
|
189
|
+
try {
|
|
190
|
+
fs.mkdirSync(path.dirname(usagePath()), { recursive: true });
|
|
191
|
+
fs.writeFileSync(usagePath(), JSON.stringify(usage, null, 2) + "\n");
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
/* a counter is never worth failing a skill run over */
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// One read of the usage file, scored per name — for the typeahead, which asks about every skill per keystroke.
|
|
198
|
+
export function skillUsageScores(now = Date.now()) {
|
|
199
|
+
const usage = readUsage();
|
|
200
|
+
return (name) => skillUsageScore(name, now, usage);
|
|
201
|
+
}
|
|
202
|
+
export function skillUsageScore(name, now = Date.now(), usage = readUsage()) {
|
|
203
|
+
const u = usage[name];
|
|
204
|
+
if (!u)
|
|
205
|
+
return 0;
|
|
206
|
+
const days = (now - u.lastUsedAt) / 86_400_000;
|
|
207
|
+
return u.usageCount * Math.max(0.5 ** (days / 7), 0.1);
|
|
208
|
+
}
|
|
209
|
+
// ---- the listing, as a <system-reminder> ------------------------------------------
|
|
210
|
+
export const wrapInSystemReminder = (content) => `<system-reminder>\n${content}\n</system-reminder>`;
|
|
211
|
+
const LISTING_HEADER = "The following skills are available for use with the skill tool:"; // Claude Code's wording, our tool name
|
|
212
|
+
const MAX_ENTRY = 250; // cap each skill's line (Claude Code: MAX_LISTING_DESC_CHARS)
|
|
213
|
+
const MIN_DESC = 20; // below this, a truncated description is noise — list the bare name
|
|
214
|
+
// One line per skill: "- name: description - when to use", as Claude Code formats it.
|
|
215
|
+
function listingEntry(s, maxDesc = MAX_ENTRY) {
|
|
216
|
+
const desc = (s.whenToUse ? `${s.description} - ${s.whenToUse}` : s.description).replace(/\s+/g, " ").trim();
|
|
217
|
+
if (maxDesc < MIN_DESC)
|
|
218
|
+
return `- ${s.name}`;
|
|
219
|
+
return `- ${s.name}: ${desc.length > maxDesc ? desc.slice(0, maxDesc - 1) + "…" : desc}`;
|
|
220
|
+
}
|
|
221
|
+
// The listing lines for a set of skills, fit into a character budget: 1% of the
|
|
222
|
+
// context window at ~4 chars/token (Claude Code's formula). Over budget, every
|
|
223
|
+
// description is shortened evenly rather than dropping skills off the end — a
|
|
224
|
+
// skill the model never hears about can never be used.
|
|
225
|
+
export function skillListing(skills, budget = Math.floor(CONFIG.contextWindow * 4 * 0.01)) {
|
|
226
|
+
let lines = skills.map((s) => listingEntry(s));
|
|
227
|
+
const total = lines.reduce((n, l) => n + l.length + 1, 0);
|
|
228
|
+
if (total > budget && skills.length) {
|
|
229
|
+
const nameCost = skills.reduce((n, s) => n + s.name.length + 5, 0); // "- " + name + ": " + "\n"
|
|
230
|
+
const maxDesc = Math.floor((budget - nameCost) / skills.length);
|
|
231
|
+
lines = skills.map((s) => listingEntry(s, maxDesc));
|
|
232
|
+
}
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
// Which listing lines has THIS conversation already received? Read it back
|
|
236
|
+
// from the history itself instead of keeping a side table: /clear, --resume,
|
|
237
|
+
// and compaction all just work — whatever the model can still see counts as
|
|
238
|
+
// sent, and nothing else does. (Claude Code keeps a per-agent "sent names" set
|
|
239
|
+
// and resets it on reload; deriving it from history gives the same answer and
|
|
240
|
+
// also re-lists after compaction drops the old reminder.)
|
|
241
|
+
function sentListingLines(messages) {
|
|
242
|
+
const sent = new Set();
|
|
243
|
+
const prefix = wrapInSystemReminder(LISTING_HEADER).split("\n").slice(0, 2).join("\n");
|
|
244
|
+
for (const m of messages) {
|
|
245
|
+
if (m.role !== "user" || typeof m.content !== "string" || !m.content.startsWith(prefix))
|
|
246
|
+
continue;
|
|
247
|
+
for (const line of m.content.split("\n"))
|
|
248
|
+
if (line.startsWith("- "))
|
|
249
|
+
sent.add(line);
|
|
250
|
+
}
|
|
251
|
+
return sent;
|
|
252
|
+
}
|
|
253
|
+
// The reminder to send before the next model call, or null if the model is up
|
|
254
|
+
// to date. Only new or changed lines go out (the delta) — the first call of a
|
|
255
|
+
// conversation gets the whole listing, an edited skill gets its fresh line.
|
|
256
|
+
export function skillListingReminder(messages, skills) {
|
|
257
|
+
const sent = sentListingLines(messages);
|
|
258
|
+
const fresh = skillListing(skills.filter((s) => !s.disableModelInvocation)).filter((l) => !sent.has(l));
|
|
259
|
+
return fresh.length ? wrapInSystemReminder(`${LISTING_HEADER}\n\n${fresh.join("\n")}`) : null;
|
|
260
|
+
}
|
|
261
|
+
// ---- the body, as its own user message ----------------------------------------------
|
|
262
|
+
// Argument substitution, Claude Code's order: $ARGUMENTS[n] / $n (shell-style
|
|
263
|
+
// split), then $ARGUMENTS whole. A body with no placeholder still gets the
|
|
264
|
+
// arguments — appended, so they are never silently dropped.
|
|
265
|
+
export function substituteArguments(body, args) {
|
|
266
|
+
const parts = args.trim() ? args.trim().split(/\s+/) : [];
|
|
267
|
+
let out = body
|
|
268
|
+
.replace(/\$ARGUMENTS\[(\d+)\]/g, (_m, i) => parts[Number(i)] ?? "")
|
|
269
|
+
.replace(/\$(\d+)(?!\d)/g, (_m, i) => parts[Number(i)] ?? "")
|
|
270
|
+
.replace(/\$ARGUMENTS/g, args);
|
|
271
|
+
if (out === body && args.trim())
|
|
272
|
+
out += `\n\nARGUMENTS: ${args}`;
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
// What the model reads once a skill is invoked (by it or by the user):
|
|
276
|
+
// "Base directory for this skill: …" so relative paths in the body resolve,
|
|
277
|
+
// then the body with arguments filled in.
|
|
278
|
+
export function skillBody(s, args = "") {
|
|
279
|
+
const body = substituteArguments(s.body, args).replaceAll("${CLAUDE_SKILL_DIR}", s.dir).replaceAll("${SKILL_DIR}", s.dir); // a copied Claude Code skill keeps working
|
|
94
280
|
const tools = s.allowedTools.length ? `\n\n[This skill should use only these tools: ${s.allowedTools.join(", ")}. Other actions still go through the normal permission gate.]` : "";
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
export function
|
|
101
|
-
const
|
|
102
|
-
|
|
281
|
+
return `Base directory for this skill: ${s.dir}\n\n${body}${tools}`;
|
|
282
|
+
}
|
|
283
|
+
// The user typed /skill <name> [args]: the two messages Claude Code produces —
|
|
284
|
+
// a <command-name> marker (so the model knows the skill is ALREADY loaded and
|
|
285
|
+
// must not call the tool again), then the body.
|
|
286
|
+
export function userSkillMessages(s, args) {
|
|
287
|
+
const argsTag = args.trim() ? `\n<command-args>${args.trim()}</command-args>` : "";
|
|
288
|
+
return [`<command-message>${s.name}</command-message>\n<command-name>/${s.name}</command-name>${argsTag}`, skillBody(s, args)];
|
|
289
|
+
}
|
|
290
|
+
// ---- the skill tool -----------------------------------------------------------------
|
|
291
|
+
const LAUNCHED = "Launching skill: "; // the tool result that tells the loop a body must follow
|
|
292
|
+
// The tool's manual is static — what skills exist arrives through the listing
|
|
293
|
+
// reminders, so a new skill never changes the tool list.
|
|
294
|
+
export const SKILL_TOOL_DESCRIPTION = `Execute a skill within the main conversation.
|
|
295
|
+
|
|
296
|
+
When users ask you to perform tasks, check if any of the available skills match. Skills provide specialized capabilities and domain knowledge.
|
|
297
|
+
|
|
298
|
+
How to invoke:
|
|
299
|
+
- Use this tool with the skill name and optional arguments
|
|
300
|
+
- Example: skill: "pdf" — invoke the pdf skill
|
|
301
|
+
- Example: skill: "commit", args: "-m 'Fix bug'" — invoke with arguments
|
|
302
|
+
|
|
303
|
+
Important:
|
|
304
|
+
- Available skills are listed in system-reminder messages in the conversation
|
|
305
|
+
- When a skill matches the user's request, invoke it BEFORE generating any other response about the task
|
|
306
|
+
- Do not invoke a skill that is already running
|
|
307
|
+
- If you see a <command-name> tag in the current conversation turn, the skill has ALREADY been loaded — follow the instructions directly instead of calling this tool again`;
|
|
308
|
+
export function buildSkillTool(getSkills = currentSkills) {
|
|
103
309
|
return {
|
|
104
310
|
definition: {
|
|
105
311
|
type: "function",
|
|
106
312
|
function: {
|
|
107
313
|
name: "skill",
|
|
108
|
-
description:
|
|
109
|
-
Available skills:
|
|
110
|
-
${listing}
|
|
111
|
-
Pass the exact skill name. If nothing matches, do not call this — just answer normally.`,
|
|
314
|
+
description: SKILL_TOOL_DESCRIPTION,
|
|
112
315
|
parameters: {
|
|
113
316
|
type: "object",
|
|
114
|
-
properties: {
|
|
115
|
-
|
|
317
|
+
properties: {
|
|
318
|
+
skill: { type: "string", description: 'The skill name. E.g., "commit", "review-pr", or "pdf"' },
|
|
319
|
+
args: { type: "string", description: "Optional arguments for the skill" },
|
|
320
|
+
},
|
|
321
|
+
required: ["skill"],
|
|
116
322
|
},
|
|
117
323
|
},
|
|
118
324
|
},
|
|
119
325
|
run: (args) => {
|
|
120
|
-
const
|
|
326
|
+
const skills = getSkills();
|
|
327
|
+
const s = findSkill(skills, args.skill ?? "");
|
|
121
328
|
if (!s)
|
|
122
|
-
return `[error]
|
|
329
|
+
return `[error] Unknown skill: ${args.skill}. Available: ${skills.filter((x) => !x.disableModelInvocation).map((x) => x.name).join(", ") || "none"}.`;
|
|
123
330
|
if (s.disableModelInvocation)
|
|
124
|
-
return `[error] Skill
|
|
125
|
-
return
|
|
331
|
+
return `[error] Skill ${s.name} cannot be used with the skill tool due to disable-model-invocation — only the user can run it.`;
|
|
332
|
+
return `${LAUNCHED}${s.name}`; // the body follows as its own message (skillBodyMessages)
|
|
126
333
|
},
|
|
127
334
|
};
|
|
128
335
|
}
|
|
336
|
+
// After a round's tool results are in: one body message per skill the model
|
|
337
|
+
// launched. They MUST come after all of the round's tool messages — the API
|
|
338
|
+
// requires every tool result of an assistant turn before any other message.
|
|
339
|
+
export function skillBodyMessages(calls, messages, skills = currentSkills()) {
|
|
340
|
+
const out = [];
|
|
341
|
+
for (const call of calls) {
|
|
342
|
+
if (call.name !== "skill")
|
|
343
|
+
continue;
|
|
344
|
+
const result = messages.find((m) => m.role === "tool" && m.tool_call_id === call.id);
|
|
345
|
+
if (!result || typeof result.content !== "string" || !result.content.startsWith(LAUNCHED))
|
|
346
|
+
continue; // refused, errored, or never ran
|
|
347
|
+
let args = {};
|
|
348
|
+
try {
|
|
349
|
+
args = JSON.parse(call.args);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const s = findSkill(skills, args.skill ?? "");
|
|
355
|
+
if (s)
|
|
356
|
+
out.push(skillBody(s, args.args ?? ""));
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
// Keep the tool list in step with the skill set: the `skill` tool exists while
|
|
361
|
+
// at least one model-invocable skill does. Called before every model call, so
|
|
362
|
+
// the first skill dropped into a folder mid-session brings the tool with it.
|
|
363
|
+
export function syncSkillTool(skills) {
|
|
364
|
+
const wanted = skills.some((s) => !s.disableModelInvocation);
|
|
365
|
+
if (wanted && !tools.skill)
|
|
366
|
+
registerExternalTool(buildSkillTool());
|
|
367
|
+
if (!wanted && tools.skill)
|
|
368
|
+
unregisterExternalTool("skill");
|
|
369
|
+
}
|
package/dist/ui.js
CHANGED
|
@@ -102,11 +102,18 @@ export function formatElapsed(ms) {
|
|
|
102
102
|
}
|
|
103
103
|
// Live activity, elapsed time and streamed tokens. The persistent status line
|
|
104
104
|
// owns the model name, so each animation need not repeat it.
|
|
105
|
-
|
|
105
|
+
//
|
|
106
|
+
// Everything that changes lives INSIDE the parentheses, after the verb. The verb
|
|
107
|
+
// must never move: a prefix that comes and goes (the old "💭 " while reasoning)
|
|
108
|
+
// shoves the whole line sideways every time it toggles. Reasoning is shown the
|
|
109
|
+
// way Claude Code does it — "thinking" while it streams, "thought for Ns" after.
|
|
110
|
+
export function spinnerText(word, elapsedSec, subAgent, tokens, thinking) {
|
|
106
111
|
const head = subAgent ? "sub-agent" : word;
|
|
107
112
|
const bits = [`${elapsedSec}s`];
|
|
108
113
|
if (tokens && tokens > 0)
|
|
109
114
|
bits.push(`↓ ${formatTokens(tokens)} tokens`);
|
|
115
|
+
if (thinking)
|
|
116
|
+
bits.push(thinking.active ? "thinking" : `thought for ${Math.max(1, Math.round(thinking.ms / 1000))}s`);
|
|
110
117
|
bits.push("Ctrl+C to interrupt");
|
|
111
118
|
return `${head}… ${chalk.dim(`(${bits.join(" · ")})`)}`;
|
|
112
119
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-from-zero",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mini-agent": "dist/agent.js"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"chalk": "^5.6.2",
|
|
28
28
|
"cli-table3": "^0.6.5",
|
|
29
|
+
"fuse.js": "^7.5.0",
|
|
29
30
|
"ink": "^5.2.1",
|
|
30
31
|
"marked": "^15.0.12",
|
|
31
32
|
"marked-terminal": "^7.3.0",
|