agent-from-zero 0.35.2 → 0.35.4
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 +14 -2
- package/dist/agent.js +46 -6
- package/dist/completion.js +94 -2
- package/dist/config.js +2 -0
- package/dist/context.js +57 -10
- package/dist/ink/app.js +218 -22
- package/dist/ink/commands.js +3 -1
- package/dist/ink/completion.js +13 -5
- package/dist/ink/launch.js +5 -0
- package/dist/ink/setup.js +10 -1
- package/dist/ink/skills-panel.js +86 -0
- package/dist/loop.js +41 -5
- package/dist/peers.js +332 -0
- package/dist/permissions.js +4 -0
- package/dist/skills.js +97 -4
- package/dist/title.js +41 -1
- package/dist/ui.js +15 -0
- package/package.json +3 -2
package/dist/skills.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 } from "./config.js"; // the
|
|
4
|
+
import { CONFIG, saveGlobalSetting } from "./config.js"; // the listing budget + /skills on/off choices
|
|
5
5
|
import { registerExternalTool, tools, unregisterExternalTool } from "./tools.js"; // a skill is exposed to the model as a tool
|
|
6
6
|
// Project skills win over global ones with the same name.
|
|
7
7
|
function skillDirs() {
|
|
@@ -37,6 +37,7 @@ export function parseSkill(raw, fallbackName, filePath = "") {
|
|
|
37
37
|
.filter(Boolean),
|
|
38
38
|
disableModelInvocation: /^true$/i.test(fm.disableModelInvocation || fm["disable-model-invocation"] || ""),
|
|
39
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]"
|
|
40
41
|
body,
|
|
41
42
|
path: filePath,
|
|
42
43
|
dir: filePath ? path.dirname(filePath) : "",
|
|
@@ -100,19 +101,111 @@ function fingerprint(dirs) {
|
|
|
100
101
|
}
|
|
101
102
|
return parts.join("|");
|
|
102
103
|
}
|
|
103
|
-
//
|
|
104
|
-
// edited, or removed since the last call. `dirs` is
|
|
105
|
-
|
|
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()) {
|
|
106
108
|
const key = dirs.join("\n");
|
|
107
109
|
const fp = fingerprint(dirs);
|
|
108
110
|
if (!cache || cache.key !== key || cache.fingerprint !== fp)
|
|
109
111
|
cache = { key, fingerprint: fp, skills: loadSkills(dirs) };
|
|
110
112
|
return cache.skills;
|
|
111
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
|
+
}
|
|
112
167
|
export function findSkill(skills, name) {
|
|
113
168
|
const n = name.trim().replace(/^\//, "").toLowerCase(); // "/deploy" and "deploy" both work, like Claude Code
|
|
114
169
|
return skills.find((s) => s.name.toLowerCase() === n);
|
|
115
170
|
}
|
|
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
|
|
184
|
+
}
|
|
185
|
+
}
|
|
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
|
+
}
|
|
116
209
|
// ---- the listing, as a <system-reminder> ------------------------------------------
|
|
117
210
|
export const wrapInSystemReminder = (content) => `<system-reminder>\n${content}\n</system-reminder>`;
|
|
118
211
|
const LISTING_HEADER = "The following skills are available for use with the skill tool:"; // Claude Code's wording, our tool name
|
package/dist/title.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentSession, nameFromTitle, nameIsPinned, setSessionInfo } from "./peers.js"; // the session name goes on the tab; the title goes to /peers
|
|
1
2
|
// One cheap model call that turns the user's first prompt into a short,
|
|
2
3
|
// pickable session name — the same idea Claude Code uses (a Haiku-tier call
|
|
3
4
|
// producing a 3-7 word sentence-case title, stored as the session's aiTitle).
|
|
@@ -18,10 +19,49 @@ export function terminalTitleSequence(title) {
|
|
|
18
19
|
}
|
|
19
20
|
// Rename the terminal tab. Silent when stdout isn't a terminal (pipes, tests,
|
|
20
21
|
// print mode) — an escape sequence in a log file is just noise.
|
|
22
|
+
//
|
|
23
|
+
// With peer sessions the tab must tell windows apart, and it must match what
|
|
24
|
+
// /peers lists. So: the title alone ("✳ Fix login bug" — the session name is
|
|
25
|
+
// derived from it), the name before there is a title ("✳ mini-agent-2"), and
|
|
26
|
+
// "name · title" only when you chose the name yourself with /rename.
|
|
27
|
+
let lastTitle; // remembered so a rename (or the end of a flash) can redraw it
|
|
28
|
+
let flashing = null;
|
|
29
|
+
export function tabTitle(title, name = currentSession()?.name, pinned = nameIsPinned()) {
|
|
30
|
+
if (title)
|
|
31
|
+
return `${TAB_PREFIX}${pinned && name ? `${name} · ` : ""}${title}`;
|
|
32
|
+
return name ? `${TAB_PREFIX}${name}` : "mini-agent";
|
|
33
|
+
}
|
|
21
34
|
export function setTerminalTitle(title) {
|
|
35
|
+
lastTitle = title;
|
|
36
|
+
nameFromTitle(title); // the session is called what it's doing (unless you named it)
|
|
37
|
+
setSessionInfo({ title: title ?? "" }); // other sessions' /peers show it too
|
|
38
|
+
if (!process.stdout.isTTY || flashing)
|
|
39
|
+
return;
|
|
40
|
+
process.stdout.write(terminalTitleSequence(tabTitle(title)));
|
|
41
|
+
}
|
|
42
|
+
export const refreshTerminalTitle = () => setTerminalTitle(lastTitle);
|
|
43
|
+
// "Which window is this?" — another session asked. Ring the bell and blink the
|
|
44
|
+
// tab title for a few seconds, then put the real title back.
|
|
45
|
+
export function flashTerminalTitle(ms = 6000) {
|
|
22
46
|
if (!process.stdout.isTTY)
|
|
23
47
|
return;
|
|
24
|
-
process.stdout.write(
|
|
48
|
+
process.stdout.write("\x07");
|
|
49
|
+
const name = currentSession()?.name ?? "mini-agent";
|
|
50
|
+
let on = false;
|
|
51
|
+
if (flashing)
|
|
52
|
+
clearInterval(flashing);
|
|
53
|
+
const stopAt = Date.now() + ms;
|
|
54
|
+
flashing = setInterval(() => {
|
|
55
|
+
on = !on;
|
|
56
|
+
if (Date.now() >= stopAt) {
|
|
57
|
+
clearInterval(flashing);
|
|
58
|
+
flashing = null;
|
|
59
|
+
process.stdout.write(terminalTitleSequence(tabTitle(lastTitle)));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
process.stdout.write(terminalTitleSequence(on ? `👋 ${name} — HERE 👋` : tabTitle(lastTitle)));
|
|
63
|
+
}, 500);
|
|
64
|
+
flashing.unref();
|
|
25
65
|
}
|
|
26
66
|
// The prompt is quoted inside <prompt> tags and the system message says so
|
|
27
67
|
// explicitly: sent bare, a chat-tuned model reads "有没有app能…?" as a question
|
package/dist/ui.js
CHANGED
|
@@ -117,6 +117,21 @@ export function spinnerText(word, elapsedSec, subAgent, tokens, thinking) {
|
|
|
117
117
|
bits.push("Ctrl+C to interrupt");
|
|
118
118
|
return `${head}… ${chalk.dim(`(${bits.join(" · ")})`)}`;
|
|
119
119
|
}
|
|
120
|
+
// The compaction progress bar, Claude Code's look:
|
|
121
|
+
// Compacting conversation…
|
|
122
|
+
// ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 8%
|
|
123
|
+
// The two lines travel as ONE status text (a spinner carries one string); the
|
|
124
|
+
// Ink spinner draws the part after the newline under the verb, ora prints it as
|
|
125
|
+
// a second line. ▰/▱ are single-width everywhere, so the bar never jitters.
|
|
126
|
+
export function progressBar(pct, width = 40) {
|
|
127
|
+
const p = Math.max(0, Math.min(100, Math.floor(pct)));
|
|
128
|
+
const filled = Math.round((p / 100) * width);
|
|
129
|
+
return chalk.white("▰".repeat(filled)) + chalk.dim("▱".repeat(width - filled)) + chalk.dim(` ${p}%`);
|
|
130
|
+
}
|
|
131
|
+
export const COMPACTING_VERB = "Compacting conversation…";
|
|
132
|
+
export function compactingText(pct, columns = process.stdout.columns || 80) {
|
|
133
|
+
return `${COMPACTING_VERB}\n${progressBar(pct, Math.max(10, Math.min(40, columns - 10)))}`;
|
|
134
|
+
}
|
|
120
135
|
// The persistent status line — model, project, branch, context use, spend, time.
|
|
121
136
|
// Pure (dir + branch passed in) so it's testable without a filesystem or git.
|
|
122
137
|
export function statusLine(model, dir, branch, ctxPct, costUsd, elapsedMs) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-from-zero",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mini-agent": "dist/agent.js"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"auto:report": "tsx eval/auto-report.ts",
|
|
18
18
|
"build": "tsc -p tsconfig.build.json",
|
|
19
19
|
"prepare": "npm run build",
|
|
20
|
-
"test": "export MINI_AGENT_REVIEW_DEBUG=0 && tsx tests/tools.test.ts && tsx tests/permissions.test.ts && tsx tests/context.test.ts && tsx tests/hooks.test.ts && tsx tests/mcp.test.ts && tsx tests/judge.test.ts && tsx tests/auto.test.ts && tsx tests/auto-context.test.ts && tsx tests/auto-providers.test.ts && tsx tests/auto-cascade.test.ts && tsx tests/auto-rules.test.ts && tsx tests/auto-rule-cascade.test.ts && tsx tests/auto-debug.test.ts && tsx tests/auto-report.test.ts && tsx tests/auto-clarification.test.ts && tsx tests/memory.test.ts && tsx tests/cost.test.ts && tsx tests/cli.test.ts && tsx tests/print.test.ts && tsx tests/status.test.ts && tsx tests/validate.test.ts && tsx tests/plan.test.ts && tsx tests/diff.test.ts && tsx tests/undo.test.ts && tsx tests/tiering.test.ts && tsx tests/mcp-http.test.ts && tsx tests/mcp-oauth.test.ts && tsx tests/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/title.test.ts && tsx tests/ui.test.ts && tsx tests/activity.test.ts && tsx tests/follow-up.test.ts && tsx tests/follow-up-ui.test.tsx && tsx tests/agents.test.ts && tsx tests/images.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/effort.test.ts && tsx tests/completion.test.ts && tsx tests/pasted-text.test.ts && tsx tests/skills.test.ts && tsx tests/markdown.test.ts && tsx tests/editor.test.ts && tsx tests/todos.test.ts && tsx tests/background.test.ts && tsx tests/team.test.ts && tsx tests/board.test.ts && tsx tests/drop.test.ts && tsx tests/editline.test.ts && tsx tests/cron.test.ts"
|
|
20
|
+
"test": "export MINI_AGENT_REVIEW_DEBUG=0 && tsx tests/tools.test.ts && tsx tests/permissions.test.ts && tsx tests/context.test.ts && tsx tests/hooks.test.ts && tsx tests/mcp.test.ts && tsx tests/judge.test.ts && tsx tests/auto.test.ts && tsx tests/auto-context.test.ts && tsx tests/auto-providers.test.ts && tsx tests/auto-cascade.test.ts && tsx tests/auto-rules.test.ts && tsx tests/auto-rule-cascade.test.ts && tsx tests/auto-debug.test.ts && tsx tests/auto-report.test.ts && tsx tests/auto-clarification.test.ts && tsx tests/memory.test.ts && tsx tests/cost.test.ts && tsx tests/cli.test.ts && tsx tests/print.test.ts && tsx tests/status.test.ts && tsx tests/validate.test.ts && tsx tests/plan.test.ts && tsx tests/diff.test.ts && tsx tests/undo.test.ts && tsx tests/tiering.test.ts && tsx tests/mcp-http.test.ts && tsx tests/mcp-oauth.test.ts && tsx tests/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/title.test.ts && tsx tests/ui.test.ts && tsx tests/activity.test.ts && tsx tests/follow-up.test.ts && tsx tests/follow-up-ui.test.tsx && tsx tests/agents.test.ts && tsx tests/images.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/effort.test.ts && tsx tests/completion.test.ts && tsx tests/pasted-text.test.ts && tsx tests/skills.test.ts && tsx tests/markdown.test.ts && tsx tests/editor.test.ts && tsx tests/todos.test.ts && tsx tests/background.test.ts && tsx tests/team.test.ts && tsx tests/peers.test.ts && tsx tests/board.test.ts && tsx tests/drop.test.ts && tsx tests/editline.test.ts && tsx tests/cron.test.ts"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20.6"
|
|
@@ -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",
|